brickfrog commited on
Commit
6604cbf
·
verified ·
1 Parent(s): 9b095d8

Upload folder using huggingface_hub

Browse files
ankigen_core/agents/integration.py CHANGED
@@ -52,71 +52,58 @@ class AgentOrchestrator:
52
  library_name: Optional[str] = None,
53
  library_topic: Optional[str] = None,
54
  generate_cloze: bool = False,
 
 
55
  ) -> Tuple[List[Card], Dict[str, Any]]:
56
- """Generate cards using the agent system"""
 
 
 
 
57
  start_time = datetime.now()
58
 
59
  try:
60
  if not self.openai_client:
61
  raise ValueError("Agent system not initialized")
62
 
63
- logger.info(f"Starting agent-based card generation: {topic} ({subject})")
64
-
65
  # Enhance context with library documentation if requested
66
  enhanced_context = context or {}
67
  library_docs = None
68
 
69
  if library_name:
70
- logger.info(f"Fetching library documentation for: {library_name}")
71
- try:
72
- context7_client = Context7Client()
73
-
74
- # Dynamic token allocation based on card generation needs
75
- # More cards need more thorough documentation
76
- base_tokens = 8000 # Increased base from 5000
77
- if num_cards > 40:
78
- token_limit = 12000 # Large card sets need more context
79
- elif num_cards > 20:
80
- token_limit = 10000 # Medium sets
81
- else:
82
- token_limit = base_tokens # Small sets
83
-
84
- # If topic is specified, we can be more focused and use fewer tokens
85
- if library_topic:
86
- token_limit = int(
87
- token_limit * 0.8
88
- ) # Can be more efficient with focused retrieval
89
-
90
- logger.info(
91
- f"Fetching {token_limit} tokens of documentation"
92
- + (f" for topic: {library_topic}" if library_topic else "")
93
- )
94
-
95
- library_docs = await context7_client.fetch_library_documentation(
96
- library_name, topic=library_topic, tokens=token_limit
97
- )
98
-
99
- if library_docs:
100
- enhanced_context["library_documentation"] = library_docs
101
- enhanced_context["library_name"] = library_name
102
- logger.info(
103
- f"Added {len(library_docs)} chars of {library_name} documentation to context"
104
- )
105
- else:
106
- logger.warning(
107
- f"Could not fetch documentation for library: {library_name}"
108
- )
109
- except Exception as e:
110
- logger.error(f"Error fetching library documentation: {e}")
111
-
112
- cards = await self._generation_phase(
113
- topic=topic,
114
- subject=subject,
115
- num_cards=num_cards,
116
- difficulty=difficulty,
117
- context=enhanced_context,
118
- generate_cloze=generate_cloze,
119
- )
120
 
121
  # Collect metadata
122
  metadata = {
@@ -128,6 +115,8 @@ class AgentOrchestrator:
128
  "difficulty": difficulty,
129
  "library_name": library_name if library_name else None,
130
  "library_docs_used": bool(library_docs),
 
 
131
  }
132
 
133
  logger.info(
@@ -139,6 +128,96 @@ class AgentOrchestrator:
139
  logger.error(f"Agent-based generation failed: {e}")
140
  raise
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  async def _generation_phase(
143
  self,
144
  topic: str,
 
52
  library_name: Optional[str] = None,
53
  library_topic: Optional[str] = None,
54
  generate_cloze: bool = False,
55
+ topics_list: Optional[List[str]] = None,
56
+ cards_per_topic: int = 8,
57
  ) -> Tuple[List[Card], Dict[str, Any]]:
58
+ """Generate cards using the agent system.
59
+
60
+ If topics_list is provided, generates cards for each subtopic separately
61
+ to ensure comprehensive coverage. Otherwise falls back to single-topic mode.
62
+ """
63
  start_time = datetime.now()
64
 
65
  try:
66
  if not self.openai_client:
67
  raise ValueError("Agent system not initialized")
68
 
 
 
69
  # Enhance context with library documentation if requested
70
  enhanced_context = context or {}
71
  library_docs = None
72
 
73
  if library_name:
74
+ library_docs = await self._fetch_library_docs(
75
+ library_name, library_topic, num_cards
76
+ )
77
+ if library_docs:
78
+ enhanced_context["library_documentation"] = library_docs
79
+ enhanced_context["library_name"] = library_name
80
+
81
+ # Generate cards - either per-topic or single-topic mode
82
+ if topics_list and len(topics_list) > 0:
83
+ logger.info(
84
+ f"Starting multi-topic generation: {len(topics_list)} topics, "
85
+ f"{cards_per_topic} cards each for '{topic}'"
86
+ )
87
+ cards = await self._generate_cards_per_topic(
88
+ main_subject=topic,
89
+ subject=subject,
90
+ topics_list=topics_list,
91
+ cards_per_topic=cards_per_topic,
92
+ difficulty=difficulty,
93
+ context=enhanced_context,
94
+ generate_cloze=generate_cloze,
95
+ )
96
+ else:
97
+ # Fallback to single-topic mode
98
+ logger.info(f"Starting single-topic generation: {topic} ({subject})")
99
+ cards = await self._generation_phase(
100
+ topic=topic,
101
+ subject=subject,
102
+ num_cards=num_cards,
103
+ difficulty=difficulty,
104
+ context=enhanced_context,
105
+ generate_cloze=generate_cloze,
106
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  # Collect metadata
109
  metadata = {
 
115
  "difficulty": difficulty,
116
  "library_name": library_name if library_name else None,
117
  "library_docs_used": bool(library_docs),
118
+ "topics_list": topics_list,
119
+ "multi_topic_mode": topics_list is not None and len(topics_list) > 0,
120
  }
121
 
122
  logger.info(
 
128
  logger.error(f"Agent-based generation failed: {e}")
129
  raise
130
 
131
+ async def _fetch_library_docs(
132
+ self, library_name: str, library_topic: Optional[str], num_cards: int
133
+ ) -> Optional[str]:
134
+ """Fetch library documentation from Context7."""
135
+ logger.info(f"Fetching library documentation for: {library_name}")
136
+ try:
137
+ context7_client = Context7Client()
138
+
139
+ # Dynamic token allocation based on card generation needs
140
+ base_tokens = 8000
141
+ if num_cards > 40:
142
+ token_limit = 12000
143
+ elif num_cards > 20:
144
+ token_limit = 10000
145
+ else:
146
+ token_limit = base_tokens
147
+
148
+ if library_topic:
149
+ token_limit = int(token_limit * 0.8)
150
+
151
+ logger.info(
152
+ f"Fetching {token_limit} tokens of documentation"
153
+ + (f" for topic: {library_topic}" if library_topic else "")
154
+ )
155
+
156
+ library_docs = await context7_client.fetch_library_documentation(
157
+ library_name, topic=library_topic, tokens=token_limit
158
+ )
159
+
160
+ if library_docs:
161
+ logger.info(
162
+ f"Added {len(library_docs)} chars of {library_name} documentation to context"
163
+ )
164
+ return library_docs
165
+ else:
166
+ logger.warning(
167
+ f"Could not fetch documentation for library: {library_name}"
168
+ )
169
+ return None
170
+ except Exception as e:
171
+ logger.error(f"Error fetching library documentation: {e}")
172
+ return None
173
+
174
+ async def _generate_cards_per_topic(
175
+ self,
176
+ main_subject: str,
177
+ subject: str,
178
+ topics_list: List[str],
179
+ cards_per_topic: int,
180
+ difficulty: str,
181
+ context: Dict[str, Any],
182
+ generate_cloze: bool,
183
+ ) -> List[Card]:
184
+ """Generate cards for each topic in the topics_list."""
185
+ all_cards: List[Card] = []
186
+ total_topics = len(topics_list)
187
+
188
+ for i, subtopic in enumerate(topics_list):
189
+ topic_num = i + 1
190
+ logger.info(
191
+ f"Generating topic {topic_num}/{total_topics}: {subtopic} "
192
+ f"({cards_per_topic} cards)"
193
+ )
194
+
195
+ # Add topic context
196
+ topic_context = {
197
+ **context,
198
+ "main_subject": main_subject,
199
+ "topic_index": topic_num,
200
+ "total_topics": total_topics,
201
+ "current_subtopic": subtopic,
202
+ }
203
+
204
+ cards = await self._generation_phase(
205
+ topic=subtopic,
206
+ subject=subject,
207
+ num_cards=cards_per_topic,
208
+ difficulty=difficulty,
209
+ context=topic_context,
210
+ generate_cloze=generate_cloze,
211
+ )
212
+
213
+ all_cards.extend(cards)
214
+ logger.info(
215
+ f"Topic {topic_num}/{total_topics} complete: {len(cards)} cards. "
216
+ f"Total: {len(all_cards)}"
217
+ )
218
+
219
+ return all_cards
220
+
221
  async def _generation_phase(
222
  self,
223
  topic: str,
ankigen_core/agents/schemas.py CHANGED
@@ -155,6 +155,12 @@ class AutoConfigSchema(BaseModel):
155
  topic_number: int = Field(
156
  ..., ge=2, le=20, description="Number of topics to generate (2-20)"
157
  )
 
 
 
 
 
 
158
  cards_per_topic: int = Field(
159
  ..., ge=2, le=30, description="Number of cards per topic (2-30)"
160
  )
 
155
  topic_number: int = Field(
156
  ..., ge=2, le=20, description="Number of topics to generate (2-20)"
157
  )
158
+ topics_list: List[str] = Field(
159
+ ...,
160
+ min_length=2,
161
+ max_length=20,
162
+ description="List of distinct subtopics to cover, ordered by learning progression",
163
+ )
164
  cards_per_topic: int = Field(
165
  ..., ge=2, le=30, description="Number of cards per topic (2-30)"
166
  )
ankigen_core/auto_config.py CHANGED
@@ -16,11 +16,30 @@ class AutoConfigService:
16
  self.context7_client = Context7Client()
17
 
18
  async def analyze_subject(
19
- self, subject: str, openai_client: AsyncOpenAI
 
 
 
20
  ) -> AutoConfigSchema:
21
- """Analyze a subject string and return configuration settings"""
22
 
23
- system_prompt = """You are an educational content analyzer specializing in spaced repetition learning. Analyze the given subject and determine flashcard generation settings that focus on ESSENTIAL concepts.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  CRITICAL PRINCIPLE: Quality over quantity. Focus on fundamental concepts that unlock understanding, not trivial facts.
26
 
@@ -32,14 +51,24 @@ Consider:
32
  - "Docker networking" → documentation_focus: "networking, network drivers, container communication"
33
  3. Identify the scope: narrow (specific feature), medium (several related topics), broad (full overview)
34
  4. Determine content type: concepts (theory/understanding), syntax (code/commands), api (library usage), practical (hands-on skills)
35
- 5. Suggest number of topics and cards - aim for thorough learning (30-60 total cards minimum)
36
  6. Recommend cloze cards for syntax/code, basic cards for concepts
37
  7. Choose model based on complexity: gpt-4.1 for complex topics, gpt-4.1-nano for basic/simple
38
 
 
 
 
 
 
 
 
 
 
 
 
39
  IMPORTANT - Focus on HIGH-VALUE topics:
40
  - GOOD topics: Core concepts, fundamental principles, mental models, design patterns, key abstractions
41
  - AVOID topics: Trivial commands (like "docker ps"), basic syntax that's easily googled, minor API details
42
- - Example: For Docker, focus on "container lifecycle", "image layers", "networking models" NOT "list of docker commands"
43
 
44
  Guidelines for settings (MINIMUM 30 cards total):
45
  - Narrow/specific scope: 4-5 essential topics with 8-10 cards each (32-50 cards)
@@ -53,12 +82,6 @@ Learning preference suggestions:
53
  - For practical: "Emphasize core patterns and principles with real-world applications"
54
  - For theory: "Build deep conceptual understanding with progressive complexity"
55
 
56
- Documentation focus examples (be specific and thorough):
57
- - "Basic Pandas Dataframe" → "dataframe creation, indexing, selection, basic operations, data types"
58
- - "React hooks" → "useState, useEffect, custom hooks, hook rules, common patterns"
59
- - "Docker basics" → "containers, images, Dockerfile, volumes, basic networking"
60
- - "TypeScript types" → "generics, conditional types, mapped types, utility types, type inference"
61
-
62
  Return a JSON object matching the AutoConfigSchema."""
63
 
64
  user_prompt = f"""Analyze this subject for flashcard generation: "{subject}"
@@ -89,10 +112,19 @@ Provide a brief rationale for your choices."""
89
  except Exception as e:
90
  logger.error(f"Failed to analyze subject: {e}")
91
  # Return sensible defaults on error (still aim for good card count)
 
92
  return AutoConfigSchema(
93
  library_search_term="",
94
  documentation_focus=None,
95
  topic_number=6,
 
 
 
 
 
 
 
 
96
  cards_per_topic=8,
97
  learning_preferences="Focus on fundamental concepts and core principles with practical examples",
98
  generate_cloze=False,
@@ -103,13 +135,21 @@ Provide a brief rationale for your choices."""
103
  )
104
 
105
  async def auto_configure(
106
- self, subject: str, openai_client: AsyncOpenAI
 
 
 
107
  ) -> Dict[str, Any]:
108
  """
109
  Complete auto-configuration pipeline:
110
  1. Analyze subject with AI
111
  2. Search Context7 for library if detected
112
  3. Return complete configuration for UI
 
 
 
 
 
113
  """
114
 
115
  if not subject or not subject.strip():
@@ -119,7 +159,9 @@ Provide a brief rationale for your choices."""
119
  logger.info(f"Starting auto-configuration for subject: '{subject}'")
120
 
121
  # Step 1: Analyze the subject
122
- config = await self.analyze_subject(subject, openai_client)
 
 
123
 
124
  # Step 2: Search Context7 for library if one was detected
125
  library_id = None
@@ -145,6 +187,7 @@ Provide a brief rationale for your choices."""
145
  "library_name": config.library_search_term if library_id else "",
146
  "library_topic": config.documentation_focus or "",
147
  "topic_number": config.topic_number,
 
148
  "cards_per_topic": config.cards_per_topic,
149
  "preference_prompt": config.learning_preferences,
150
  "generate_cloze_checkbox": config.generate_cloze,
 
16
  self.context7_client = Context7Client()
17
 
18
  async def analyze_subject(
19
+ self,
20
+ subject: str,
21
+ openai_client: AsyncOpenAI,
22
+ target_topic_count: int | None = None,
23
  ) -> AutoConfigSchema:
24
+ """Analyze a subject string and return configuration settings.
25
 
26
+ Args:
27
+ subject: The subject to analyze
28
+ openai_client: OpenAI client for LLM calls
29
+ target_topic_count: If provided, forces exactly this many topics in decomposition
30
+ """
31
+
32
+ # Build topic count instruction if override provided
33
+ topic_count_instruction = ""
34
+ if target_topic_count is not None:
35
+ topic_count_instruction = f"""
36
+ IMPORTANT OVERRIDE: The user has requested exactly {target_topic_count} topics.
37
+ You MUST set topic_number to {target_topic_count} and provide exactly {target_topic_count} items in topics_list.
38
+ Choose the {target_topic_count} most important/foundational subtopics for this subject.
39
+ """
40
+
41
+ system_prompt = f"""You are an educational content analyzer specializing in spaced repetition learning. Analyze the given subject and determine flashcard generation settings that focus on ESSENTIAL concepts.
42
+ {topic_count_instruction}
43
 
44
  CRITICAL PRINCIPLE: Quality over quantity. Focus on fundamental concepts that unlock understanding, not trivial facts.
45
 
 
51
  - "Docker networking" → documentation_focus: "networking, network drivers, container communication"
52
  3. Identify the scope: narrow (specific feature), medium (several related topics), broad (full overview)
53
  4. Determine content type: concepts (theory/understanding), syntax (code/commands), api (library usage), practical (hands-on skills)
54
+ 5. TOPIC DECOMPOSITION: Break down the subject into distinct subtopics that together provide comprehensive coverage
55
  6. Recommend cloze cards for syntax/code, basic cards for concepts
56
  7. Choose model based on complexity: gpt-4.1 for complex topics, gpt-4.1-nano for basic/simple
57
 
58
+ TOPIC DECOMPOSITION (topics_list):
59
+ You MUST provide a topics_list - a list of distinct subtopics that together cover the subject comprehensively.
60
+ - Each topic should be specific and non-overlapping
61
+ - Order topics from foundational to advanced (learning progression)
62
+ - The number of topics should match topic_number
63
+
64
+ Examples:
65
+ - "React Hooks" → topics_list: ["useState fundamentals", "useEffect and lifecycle", "useRef and useContext", "custom hooks patterns", "performance with useMemo/useCallback", "testing hooks"]
66
+ - "Docker basics" → topics_list: ["containers vs VMs", "images and Dockerfile", "container lifecycle", "volumes and persistence", "networking fundamentals", "docker-compose basics"]
67
+ - "Machine Learning" → topics_list: ["supervised vs unsupervised", "regression models", "classification models", "model evaluation metrics", "overfitting and regularization", "feature engineering", "cross-validation"]
68
+
69
  IMPORTANT - Focus on HIGH-VALUE topics:
70
  - GOOD topics: Core concepts, fundamental principles, mental models, design patterns, key abstractions
71
  - AVOID topics: Trivial commands (like "docker ps"), basic syntax that's easily googled, minor API details
 
72
 
73
  Guidelines for settings (MINIMUM 30 cards total):
74
  - Narrow/specific scope: 4-5 essential topics with 8-10 cards each (32-50 cards)
 
82
  - For practical: "Emphasize core patterns and principles with real-world applications"
83
  - For theory: "Build deep conceptual understanding with progressive complexity"
84
 
 
 
 
 
 
 
85
  Return a JSON object matching the AutoConfigSchema."""
86
 
87
  user_prompt = f"""Analyze this subject for flashcard generation: "{subject}"
 
112
  except Exception as e:
113
  logger.error(f"Failed to analyze subject: {e}")
114
  # Return sensible defaults on error (still aim for good card count)
115
+ # Use the subject as a single topic as fallback
116
  return AutoConfigSchema(
117
  library_search_term="",
118
  documentation_focus=None,
119
  topic_number=6,
120
+ topics_list=[
121
+ f"{subject} - fundamentals",
122
+ f"{subject} - core concepts",
123
+ f"{subject} - practical applications",
124
+ f"{subject} - common patterns",
125
+ f"{subject} - best practices",
126
+ f"{subject} - advanced topics",
127
+ ],
128
  cards_per_topic=8,
129
  learning_preferences="Focus on fundamental concepts and core principles with practical examples",
130
  generate_cloze=False,
 
135
  )
136
 
137
  async def auto_configure(
138
+ self,
139
+ subject: str,
140
+ openai_client: AsyncOpenAI,
141
+ target_topic_count: int | None = None,
142
  ) -> Dict[str, Any]:
143
  """
144
  Complete auto-configuration pipeline:
145
  1. Analyze subject with AI
146
  2. Search Context7 for library if detected
147
  3. Return complete configuration for UI
148
+
149
+ Args:
150
+ subject: The subject to analyze
151
+ openai_client: OpenAI client for LLM calls
152
+ target_topic_count: If provided, forces exactly this many topics
153
  """
154
 
155
  if not subject or not subject.strip():
 
159
  logger.info(f"Starting auto-configuration for subject: '{subject}'")
160
 
161
  # Step 1: Analyze the subject
162
+ config = await self.analyze_subject(
163
+ subject, openai_client, target_topic_count=target_topic_count
164
+ )
165
 
166
  # Step 2: Search Context7 for library if one was detected
167
  library_id = None
 
187
  "library_name": config.library_search_term if library_id else "",
188
  "library_topic": config.documentation_focus or "",
189
  "topic_number": config.topic_number,
190
+ "topics_list": config.topics_list,
191
  "cards_per_topic": config.cards_per_topic,
192
  "preference_prompt": config.learning_preferences,
193
  "generate_cloze_checkbox": config.generate_cloze,
ankigen_core/card_generator.py CHANGED
@@ -52,11 +52,6 @@ GENERATION_MODES = [
52
  "label": "Single Subject",
53
  "description": "Generate cards for a specific topic",
54
  },
55
- {
56
- "value": "path",
57
- "label": "Learning Path",
58
- "description": "Break down a job description or learning goal into subjects",
59
- },
60
  {
61
  "value": "text",
62
  "label": "From Text",
@@ -140,6 +135,7 @@ async def orchestrate_card_generation(
140
  use_llm_judge: bool = False,
141
  library_name: str = None,
142
  library_topic: str = None,
 
143
  ):
144
  """Orchestrates the card generation process based on UI inputs."""
145
  logger.info(f"Starting card generation orchestration in {generation_mode} mode")
@@ -175,6 +171,8 @@ async def orchestrate_card_generation(
175
  library_name=library_name,
176
  library_topic=library_topic,
177
  generate_cloze=generate_cloze,
 
 
178
  )
179
 
180
  token_usage_html = _get_token_usage_html(token_tracker)
 
52
  "label": "Single Subject",
53
  "description": "Generate cards for a specific topic",
54
  },
 
 
 
 
 
55
  {
56
  "value": "text",
57
  "label": "From Text",
 
135
  use_llm_judge: bool = False,
136
  library_name: str = None,
137
  library_topic: str = None,
138
+ topics_list: List[str] = None,
139
  ):
140
  """Orchestrates the card generation process based on UI inputs."""
141
  logger.info(f"Starting card generation orchestration in {generation_mode} mode")
 
171
  library_name=library_name,
172
  library_topic=library_topic,
173
  generate_cloze=generate_cloze,
174
+ topics_list=topics_list,
175
+ cards_per_topic=cards_per_topic,
176
  )
177
 
178
  token_usage_html = _get_token_usage_html(token_tracker)
ankigen_core/cli.py CHANGED
@@ -13,6 +13,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
13
  from rich.table import Table
14
  from rich.panel import Panel
15
 
 
16
  from ankigen_core.auto_config import AutoConfigService
17
  from ankigen_core.card_generator import orchestrate_card_generation
18
  from ankigen_core.exporters import export_dataframe_to_apkg, export_dataframe_to_csv
@@ -55,13 +56,13 @@ async def auto_configure_from_prompt(
55
  await client_manager.initialize_client(api_key)
56
  openai_client = client_manager.get_client()
57
 
58
- # Get auto-config
59
  auto_config_service = AutoConfigService()
60
- config = await auto_config_service.auto_configure(prompt, openai_client)
 
 
61
 
62
- # Apply overrides
63
- if override_topics is not None:
64
- config["topic_number"] = override_topics
65
  if override_cards is not None:
66
  config["cards_per_topic"] = override_cards
67
  if override_model is not None:
@@ -86,6 +87,17 @@ async def auto_configure_from_prompt(
86
  table.add_row("Library", config.get("library_name"))
87
  if config.get("library_topic"):
88
  table.add_row("Library Topic", config.get("library_topic"))
 
 
 
 
 
 
 
 
 
 
 
89
  if config.get("preference_prompt"):
90
  table.add_row(
91
  "Learning Focus", config.get("preference_prompt", "")[:50] + "..."
@@ -142,6 +154,7 @@ async def generate_cards_from_config(
142
  library_topic=config.get("library_topic")
143
  if config.get("library_topic")
144
  else None,
 
145
  )
146
 
147
  progress.update(task, completed=100)
@@ -327,13 +340,17 @@ def main(
327
  summary.add_row("Output File:", f"[bold]{exported_path}[/bold]")
328
  summary.add_row("File Size:", f"{file_size:.1f} KB")
329
 
330
- # Extract token count from HTML if available
331
- if "tokens" in token_html.lower():
332
- import re
333
-
334
- token_match = re.search(r"(\d+)\s*tokens", token_html)
335
- if token_match:
336
- summary.add_row("Tokens Used:", token_match.group(1))
 
 
 
 
337
 
338
  console.print(
339
  Panel(summary, border_style="green", title="Generation Complete")
 
13
  from rich.table import Table
14
  from rich.panel import Panel
15
 
16
+ from ankigen_core.agents.token_tracker import get_token_tracker
17
  from ankigen_core.auto_config import AutoConfigService
18
  from ankigen_core.card_generator import orchestrate_card_generation
19
  from ankigen_core.exporters import export_dataframe_to_apkg, export_dataframe_to_csv
 
56
  await client_manager.initialize_client(api_key)
57
  openai_client = client_manager.get_client()
58
 
59
+ # Get auto-config (pass topic count override so LLM decomposes correctly)
60
  auto_config_service = AutoConfigService()
61
+ config = await auto_config_service.auto_configure(
62
+ prompt, openai_client, target_topic_count=override_topics
63
+ )
64
 
65
+ # Apply remaining overrides (topics already handled in auto_configure)
 
 
66
  if override_cards is not None:
67
  config["cards_per_topic"] = override_cards
68
  if override_model is not None:
 
87
  table.add_row("Library", config.get("library_name"))
88
  if config.get("library_topic"):
89
  table.add_row("Library Topic", config.get("library_topic"))
90
+
91
+ # Display discovered topics
92
+ if config.get("topics_list"):
93
+ topics = config["topics_list"]
94
+ # Show first few topics, indicate if there are more
95
+ if len(topics) <= 4:
96
+ topics_str = ", ".join(topics)
97
+ else:
98
+ topics_str = ", ".join(topics[:3]) + f", ... (+{len(topics) - 3} more)"
99
+ table.add_row("Subtopics", topics_str)
100
+
101
  if config.get("preference_prompt"):
102
  table.add_row(
103
  "Learning Focus", config.get("preference_prompt", "")[:50] + "..."
 
154
  library_topic=config.get("library_topic")
155
  if config.get("library_topic")
156
  else None,
157
+ topics_list=config.get("topics_list"),
158
  )
159
 
160
  progress.update(task, completed=100)
 
340
  summary.add_row("Output File:", f"[bold]{exported_path}[/bold]")
341
  summary.add_row("File Size:", f"{file_size:.1f} KB")
342
 
343
+ # Get token usage from tracker
344
+ tracker = get_token_tracker()
345
+ session = tracker.get_session_summary()
346
+ if session["total_tokens"] > 0:
347
+ # Calculate totals across all models
348
+ total_input = sum(u.prompt_tokens for u in tracker.usage_history)
349
+ total_output = sum(u.completion_tokens for u in tracker.usage_history)
350
+ summary.add_row(
351
+ "Tokens:",
352
+ f"{total_input:,} in / {total_output:,} out ({session['total_tokens']:,} total)",
353
+ )
354
 
355
  console.print(
356
  Panel(summary, border_style="green", title="Generation Complete")
ankigen_core/ui_logic.py CHANGED
@@ -1,7 +1,7 @@
1
  # Module for functions that build or manage UI sections/logic
2
 
3
  import gradio as gr
4
- import pandas as pd # Needed for use_selected_subjects type hinting
5
  from typing import (
6
  Callable,
7
  List,
@@ -51,24 +51,19 @@ crawler_ui_logger = get_logger() # Keep this definition
51
  def update_mode_visibility(
52
  mode: str,
53
  current_subject: str,
54
- current_description: str,
55
  current_text: str,
56
  current_url: str,
57
  ):
58
  """Updates visibility and values of UI elements based on generation mode."""
59
  is_subject = mode == "subject"
60
- is_path = mode == "path"
61
  is_text = mode == "text"
62
  is_web = mode == "web"
63
 
64
  # Determine value persistence or clearing
65
  subject_val = current_subject if is_subject else ""
66
- description_val = current_description if is_path else ""
67
  text_val = current_text if is_text else ""
68
  url_val = current_url if is_web else ""
69
 
70
- cards_output_visible = is_subject or is_text or is_web
71
-
72
  # Define standard columns for empty DataFrames
73
  main_output_df_columns = [
74
  "Index",
@@ -82,173 +77,22 @@ def update_mode_visibility(
82
  "Learning_Outcomes",
83
  "Difficulty",
84
  ]
85
- subjects_list_df_columns = ["Subject", "Prerequisites", "Time Estimate"]
86
 
87
  return (
88
  gr.update(visible=is_subject), # 1 subject_mode (Group)
89
- gr.update(visible=is_path), # 2 path_mode (Group)
90
- gr.update(visible=is_text), # 3 text_mode (Group)
91
- gr.update(visible=is_web), # 4 web_mode (Group for crawler UI)
92
- gr.update(visible=is_path), # 5 path_results (Group)
93
- gr.update(
94
- visible=cards_output_visible
95
- ), # 6 cards_output (Group for main table)
96
- gr.update(value=subject_val), # Now 7th item (was 8th)
97
- gr.update(value=description_val), # Now 8th item (was 9th)
98
- gr.update(value=text_val), # Now 9th item (was 10th)
99
- gr.update(value=url_val), # Now 10th item (was 11th)
100
  gr.update(
101
  value=pd.DataFrame(columns=main_output_df_columns)
102
- ), # Now 11th item (was 12th)
103
- gr.update(
104
- value=pd.DataFrame(columns=subjects_list_df_columns)
105
- ), # Now 12th item (was 13th)
106
- gr.update(value=""), # Now 13th item (was 14th)
107
- gr.update(value=""), # Now 14th item (was 15th)
108
  gr.update(
109
  value="<div><b>Total Cards Generated:</b> <span id='total-cards-count'>0</span></div>",
110
  visible=False,
111
- ), # Now 15th item (was 16th)
112
- )
113
-
114
-
115
- def use_selected_subjects(subjects_df: pd.DataFrame | None):
116
- """Updates UI to use subjects from learning path analysis."""
117
- if subjects_df is None or subjects_df.empty:
118
- gr.Warning("No subjects available to copy from Learning Path analysis.")
119
- # Return updates that change nothing for all 18 outputs
120
- return (
121
- gr.update(), # 1 generation_mode
122
- gr.update(), # 2 subject_mode
123
- gr.update(), # 3 path_mode
124
- gr.update(), # 4 text_mode
125
- gr.update(), # 5 web_mode
126
- gr.update(), # 6 path_results
127
- gr.update(), # 7 cards_output
128
- gr.update(), # 8 subject
129
- gr.update(), # 9 description
130
- gr.update(), # 10 source_text
131
- gr.update(), # 11 web_crawl_url_input
132
- gr.update(), # 12 topic_number
133
- gr.update(), # 13 preference_prompt
134
- gr.update(
135
- value=pd.DataFrame(
136
- columns=[
137
- "Index",
138
- "Topic",
139
- "Card_Type",
140
- "Question",
141
- "Answer",
142
- "Explanation",
143
- "Example",
144
- "Prerequisites",
145
- "Learning_Outcomes",
146
- "Difficulty",
147
- ]
148
- )
149
- ), # 14 output (DataFrame)
150
- gr.update(
151
- value=pd.DataFrame(
152
- columns=["Subject", "Prerequisites", "Time Estimate"]
153
- )
154
- ), # 15 subjects_list (DataFrame)
155
- gr.update(), # 16 learning_order
156
- gr.update(), # 17 projects
157
- gr.update(visible=False), # 18 total_cards_html
158
- )
159
-
160
- try:
161
- subjects = subjects_df["Subject"].tolist()
162
- combined_subject = ", ".join(subjects)
163
- # Ensure suggested_topics is an int, Gradio sliders expect int/float for value
164
- suggested_topics = int(min(len(subjects) + 1, 20))
165
- except KeyError:
166
- gr.Error("Learning path analysis result is missing the 'Subject' column.")
167
- # Return no-change updates for all 18 outputs
168
- return (
169
- gr.update(), # 1 generation_mode
170
- gr.update(), # 2 subject_mode
171
- gr.update(), # 3 path_mode
172
- gr.update(), # 4 text_mode
173
- gr.update(), # 5 web_mode
174
- gr.update(), # 6 path_results
175
- gr.update(), # 7 cards_output
176
- gr.update(), # 8 subject
177
- gr.update(), # 9 description
178
- gr.update(), # 10 source_text
179
- gr.update(), # 11 web_crawl_url_input
180
- gr.update(), # 12 topic_number
181
- gr.update(), # 13 preference_prompt
182
- gr.update(
183
- value=pd.DataFrame(
184
- columns=[
185
- "Index",
186
- "Topic",
187
- "Card_Type",
188
- "Question",
189
- "Answer",
190
- "Explanation",
191
- "Example",
192
- "Prerequisites",
193
- "Learning_Outcomes",
194
- "Difficulty",
195
- ]
196
- )
197
- ), # 14 output (DataFrame)
198
- gr.update(
199
- value=pd.DataFrame(
200
- columns=["Subject", "Prerequisites", "Time Estimate"]
201
- )
202
- ), # 15 subjects_list (DataFrame)
203
- gr.update(), # 16 learning_order
204
- gr.update(), # 17 projects
205
- gr.update(visible=False), # 18 total_cards_html
206
- )
207
-
208
- # Corresponds to outputs in app.py for use_subjects.click:
209
- # [generation_mode, subject_mode, path_mode, text_mode, web_mode, path_results, cards_output,
210
- # subject, description, source_text, web_crawl_url_input, topic_number, preference_prompt,
211
- # output, subjects_list, learning_order, projects, total_cards_html]
212
- return (
213
- gr.update(value="subject"), # 1 generation_mode (Radio)
214
- gr.update(visible=True), # 2 subject_mode (Group)
215
- gr.update(visible=False), # 3 path_mode (Group)
216
- gr.update(visible=False), # 4 text_mode (Group)
217
- gr.update(visible=False), # 5 web_mode (Group)
218
- gr.update(visible=False), # 6 path_results (Group)
219
- gr.update(visible=True), # 7 cards_output (Group)
220
- gr.update(value=combined_subject), # 8 subject (Textbox)
221
- gr.update(value=""), # 9 description (Textbox)
222
- gr.update(value=""), # 10 source_text (Textbox)
223
- gr.update(value=""), # 11 web_crawl_url_input (Textbox)
224
- gr.update(value=suggested_topics), # 12 topic_number (Slider)
225
- gr.update(
226
- value="Focus on connections between these subjects and their practical applications."
227
- ), # 13 preference_prompt (Textbox)
228
- gr.update(
229
- value=pd.DataFrame(
230
- columns=[
231
- "Index",
232
- "Topic",
233
- "Card_Type",
234
- "Question",
235
- "Answer",
236
- "Explanation",
237
- "Example",
238
- "Prerequisites",
239
- "Learning_Outcomes",
240
- "Difficulty",
241
- ]
242
- )
243
- ), # 14 output (DataFrame) - Clear it
244
- gr.update(
245
- value=subjects_df
246
- ), # 15 subjects_list (DataFrame) - Keep the value that triggered this
247
- gr.update(
248
- value=""
249
- ), # 16 learning_order (Markdown) - Clear it or decide to keep
250
- gr.update(value=""), # 17 projects (Markdown) - Clear it or decide to keep
251
- gr.update(visible=False), # 18 total_cards_html (HTML)
252
  )
253
 
254
 
 
1
  # Module for functions that build or manage UI sections/logic
2
 
3
  import gradio as gr
4
+ import pandas as pd
5
  from typing import (
6
  Callable,
7
  List,
 
51
  def update_mode_visibility(
52
  mode: str,
53
  current_subject: str,
 
54
  current_text: str,
55
  current_url: str,
56
  ):
57
  """Updates visibility and values of UI elements based on generation mode."""
58
  is_subject = mode == "subject"
 
59
  is_text = mode == "text"
60
  is_web = mode == "web"
61
 
62
  # Determine value persistence or clearing
63
  subject_val = current_subject if is_subject else ""
 
64
  text_val = current_text if is_text else ""
65
  url_val = current_url if is_web else ""
66
 
 
 
67
  # Define standard columns for empty DataFrames
68
  main_output_df_columns = [
69
  "Index",
 
77
  "Learning_Outcomes",
78
  "Difficulty",
79
  ]
 
80
 
81
  return (
82
  gr.update(visible=is_subject), # 1 subject_mode (Group)
83
+ gr.update(visible=is_text), # 2 text_mode (Group)
84
+ gr.update(visible=is_web), # 3 web_mode (Group for crawler UI)
85
+ gr.update(visible=True), # 4 cards_output (always visible now)
86
+ gr.update(value=subject_val), # 5 subject
87
+ gr.update(value=text_val), # 6 source_text
88
+ gr.update(value=url_val), # 7 web_crawl_url_input
 
 
 
 
 
89
  gr.update(
90
  value=pd.DataFrame(columns=main_output_df_columns)
91
+ ), # 8 output (DataFrame)
 
 
 
 
 
92
  gr.update(
93
  value="<div><b>Total Cards Generated:</b> <span id='total-cards-count'>0</span></div>",
94
  visible=False,
95
+ ), # 9 total_cards_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  )
97
 
98
 
app.py CHANGED
@@ -15,7 +15,6 @@ from ankigen_core.exporters import (
15
  export_dataframe_to_apkg,
16
  export_dataframe_to_csv,
17
  ) # Anki models (BASIC_MODEL, CLOZE_MODEL) are internal to exporters
18
- from ankigen_core.learning_path import analyze_learning_path
19
  from ankigen_core.llm_interface import (
20
  OpenAIClientManager,
21
  ) # structured_output_completion is internal to core modules
@@ -23,7 +22,6 @@ from ankigen_core.ui_logic import (
23
  crawl_and_generate,
24
  create_crawler_main_mode_elements,
25
  update_mode_visibility,
26
- use_selected_subjects,
27
  )
28
  from ankigen_core.utils import (
29
  ResponseCache,
@@ -68,6 +66,16 @@ except (AttributeError, ImportError):
68
  # Fallback for older gradio versions or when themes are not available
69
  custom_theme = None
70
 
 
 
 
 
 
 
 
 
 
 
71
  # --- Example Data for Initialization ---
72
  example_data = pd.DataFrame(
73
  [
@@ -139,48 +147,8 @@ def get_recent_logs(logger_name="ankigen") -> str:
139
 
140
  def create_ankigen_interface():
141
  logger.info("Creating AnkiGen Gradio interface...")
142
- with gr.Blocks(
143
- theme=custom_theme,
144
- title="AnkiGen",
145
- css="""
146
- #footer {display:none !important}
147
- .tall-dataframe {min-height: 500px !important}
148
- .contain {max-width: 100% !important; margin: auto;}
149
- .output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
150
- .hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
151
- .export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; }
152
-
153
- /* REMOVING CSS previously intended for DataFrame readability to ensure plain text */
154
- /*
155
- .explanation-text {
156
- background: #f0fdf4;
157
- border-left: 3px solid #4ade80;
158
- padding: 0.5em;
159
- margin-bottom: 0.5em;
160
- border-radius: 4px;
161
- }
162
- .example-text-plain {
163
- background: #fff7ed;
164
- border-left: 3px solid #f97316;
165
- padding: 0.5em;
166
- margin-bottom: 0.5em;
167
- border-radius: 4px;
168
- }
169
- pre code {
170
- display: block;
171
- padding: 0.8em;
172
- background: #1e293b;
173
- color: #e2e8f0;
174
- border-radius: 4px;
175
- overflow-x: auto;
176
- font-family: 'Fira Code', 'Consolas', monospace;
177
- font-size: 0.9em;
178
- margin-bottom: 0.5em;
179
- }
180
- */
181
- """,
182
- js=js_storage,
183
- ) as ankigen:
184
  with gr.Column(elem_classes="contain"):
185
  gr.Markdown("# 📚 AnkiGen - Anki Card Generator")
186
  gr.Markdown("#### Generate Anki flashcards using AI.")
@@ -208,16 +176,6 @@ def create_ankigen_interface():
208
  "Auto-fill",
209
  variant="secondary",
210
  )
211
- with gr.Group(visible=False) as path_mode:
212
- description = gr.Textbox(
213
- label="Learning Goal",
214
- placeholder="Paste a job description...",
215
- lines=5,
216
- )
217
- analyze_button = gr.Button(
218
- "Analyze & Break Down",
219
- variant="secondary",
220
- )
221
  with gr.Group(visible=False) as text_mode:
222
  source_text = gr.Textbox(
223
  label="Source Text",
@@ -329,21 +287,6 @@ def create_ankigen_interface():
329
 
330
  generate_button = gr.Button("Generate Cards", variant="primary")
331
 
332
- with gr.Group(visible=False) as path_results:
333
- gr.Markdown("### Learning Path Analysis")
334
- subjects_list = gr.Dataframe(
335
- headers=["Subject", "Prerequisites", "Time Estimate"],
336
- label="Recommended Subjects",
337
- interactive=False,
338
- )
339
- learning_order = gr.Markdown("### Recommended Learning Order")
340
- projects = gr.Markdown("### Suggested Projects")
341
- use_subjects = gr.Button("Use These Subjects ℹ️", variant="primary")
342
- gr.Markdown(
343
- "*Click to copy subjects to main input*",
344
- elem_classes="hint-text",
345
- )
346
-
347
  with gr.Group() as cards_output:
348
  gr.Markdown("### Generated Cards")
349
  with gr.Accordion("Output Format", open=False):
@@ -421,93 +364,18 @@ def create_ankigen_interface():
421
  inputs=[
422
  generation_mode,
423
  subject,
424
- description,
425
  source_text,
426
  web_crawl_url_input,
427
  ],
428
  outputs=[
429
  subject_mode,
430
- path_mode,
431
- text_mode,
432
- web_mode,
433
- path_results,
434
- cards_output,
435
- subject,
436
- description,
437
- source_text,
438
- web_crawl_url_input,
439
- output,
440
- subjects_list,
441
- learning_order,
442
- projects,
443
- total_cards_html,
444
- ],
445
- )
446
-
447
- # Define an async wrapper for the analyze_learning_path partial
448
- async def handle_analyze_click(
449
- api_key_val,
450
- description_val,
451
- model_choice_val,
452
- progress=gr.Progress(track_tqdm=True), # Added progress tracker
453
- ):
454
- try:
455
- # Call analyze_learning_path directly, as client_manager and response_cache are in scope
456
- return await analyze_learning_path(
457
- client_manager, # from global scope
458
- response_cache, # from global scope
459
- api_key_val,
460
- description_val,
461
- model_choice_val,
462
- )
463
- except gr.Error as e: # Catch the specific Gradio error
464
- logger.error(f"Learning path analysis failed: {e}", exc_info=True)
465
- # Re-raise the error so Gradio displays it to the user
466
- # And return appropriate empty updates for the outputs
467
- # to prevent a subsequent Gradio error about mismatched return values.
468
- gr.Error(str(e)) # This will be shown in the UI.
469
- empty_subjects_df = pd.DataFrame(
470
- columns=["Subject", "Prerequisites", "Time Estimate"],
471
- )
472
- return (
473
- gr.update(
474
- value=empty_subjects_df,
475
- ), # For subjects_list (DataFrame)
476
- gr.update(value=""), # For learning_order (Markdown)
477
- gr.update(value=""), # For projects (Markdown)
478
- )
479
-
480
- analyze_button.click(
481
- fn=handle_analyze_click, # MODIFIED: Use the new async handler
482
- inputs=[
483
- api_key_input,
484
- description,
485
- model_choice,
486
- ],
487
- outputs=[subjects_list, learning_order, projects],
488
- )
489
-
490
- use_subjects.click(
491
- fn=use_selected_subjects,
492
- inputs=[subjects_list],
493
- outputs=[
494
- generation_mode,
495
- subject_mode,
496
- path_mode,
497
  text_mode,
498
  web_mode,
499
- path_results,
500
  cards_output,
501
  subject,
502
- description,
503
  source_text,
504
  web_crawl_url_input,
505
- topic_number,
506
- preference_prompt,
507
  output,
508
- subjects_list,
509
- learning_order,
510
- projects,
511
  total_cards_html,
512
  ],
513
  )
@@ -920,13 +788,21 @@ if __name__ == "__main__":
920
  logger.info("Launching AnkiGen Gradio interface...")
921
 
922
  # Configure for HuggingFace Spaces vs local development
 
 
 
 
 
 
923
  if os.environ.get("SPACE_ID"): # On HuggingFace Spaces
924
  # Let HuggingFace handle all the configuration
925
- ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch()
 
 
926
  else: # Local development
927
  # Use auto port finding for local dev
928
  ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
929
- server_name="127.0.0.1", share=False
930
  )
931
  except Exception as e:
932
  logger.critical(f"Failed to launch Gradio interface: {e}", exc_info=True)
 
15
  export_dataframe_to_apkg,
16
  export_dataframe_to_csv,
17
  ) # Anki models (BASIC_MODEL, CLOZE_MODEL) are internal to exporters
 
18
  from ankigen_core.llm_interface import (
19
  OpenAIClientManager,
20
  ) # structured_output_completion is internal to core modules
 
22
  crawl_and_generate,
23
  create_crawler_main_mode_elements,
24
  update_mode_visibility,
 
25
  )
26
  from ankigen_core.utils import (
27
  ResponseCache,
 
66
  # Fallback for older gradio versions or when themes are not available
67
  custom_theme = None
68
 
69
+ # CSS for the interface (moved to module level for Gradio 6 compatibility)
70
+ custom_css = """
71
+ #footer {display:none !important}
72
+ .tall-dataframe {min-height: 500px !important}
73
+ .contain {max-width: 100% !important; margin: auto;}
74
+ .output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
75
+ .hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
76
+ .export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; }
77
+ """
78
+
79
  # --- Example Data for Initialization ---
80
  example_data = pd.DataFrame(
81
  [
 
147
 
148
  def create_ankigen_interface():
149
  logger.info("Creating AnkiGen Gradio interface...")
150
+ # Note: theme, css, and js moved to .launch() for Gradio 6 compatibility
151
+ with gr.Blocks(title="AnkiGen") as ankigen:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  with gr.Column(elem_classes="contain"):
153
  gr.Markdown("# 📚 AnkiGen - Anki Card Generator")
154
  gr.Markdown("#### Generate Anki flashcards using AI.")
 
176
  "Auto-fill",
177
  variant="secondary",
178
  )
 
 
 
 
 
 
 
 
 
 
179
  with gr.Group(visible=False) as text_mode:
180
  source_text = gr.Textbox(
181
  label="Source Text",
 
287
 
288
  generate_button = gr.Button("Generate Cards", variant="primary")
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  with gr.Group() as cards_output:
291
  gr.Markdown("### Generated Cards")
292
  with gr.Accordion("Output Format", open=False):
 
364
  inputs=[
365
  generation_mode,
366
  subject,
 
367
  source_text,
368
  web_crawl_url_input,
369
  ],
370
  outputs=[
371
  subject_mode,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  text_mode,
373
  web_mode,
 
374
  cards_output,
375
  subject,
 
376
  source_text,
377
  web_crawl_url_input,
 
 
378
  output,
 
 
 
379
  total_cards_html,
380
  ],
381
  )
 
788
  logger.info("Launching AnkiGen Gradio interface...")
789
 
790
  # Configure for HuggingFace Spaces vs local development
791
+ # Note: theme, css, js moved to launch() for Gradio 6 compatibility
792
+ launch_kwargs = {
793
+ "theme": custom_theme,
794
+ "css": custom_css,
795
+ "js": js_storage,
796
+ }
797
  if os.environ.get("SPACE_ID"): # On HuggingFace Spaces
798
  # Let HuggingFace handle all the configuration
799
+ ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
800
+ **launch_kwargs
801
+ )
802
  else: # Local development
803
  # Use auto port finding for local dev
804
  ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
805
+ server_name="127.0.0.1", share=False, **launch_kwargs
806
  )
807
  except Exception as e:
808
  logger.critical(f"Failed to launch Gradio interface: {e}", exc_info=True)