milwright commited on
Commit
3ff649e
Β·
verified Β·
1 Parent(s): d8814fa

Upload 4 files

Browse files
Files changed (3) hide show
  1. README.md +9 -3
  2. app.py +379 -332
  3. config.json +3 -3
README.md CHANGED
@@ -8,12 +8,12 @@ sdk_version: 5.39.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Interactive STEM adventure game guide
12
  ---
13
 
14
  # STEM Adventure Games
15
 
16
- Interactive STEM adventure game guide
17
 
18
  ## Quick Setup
19
 
@@ -31,6 +31,12 @@ Interactive STEM adventure game guide
31
  5. This enables automatic configuration updates
32
 
33
 
 
 
 
 
 
 
34
 
35
  ### Step 3: Test Your Space
36
  Your Space should now be running! Try the example prompts or ask your own questions.
@@ -39,7 +45,7 @@ Your Space should now be running! Try the example prompts or ask your own questi
39
  - **Model**: google/gemini-2.0-flash-001
40
  - **API Key Variable**: API_KEY
41
  - **HF Token Variable**: HF_TOKEN (for auto-updates)
42
- - **Access**: Public
43
 
44
  ## Support
45
  For help, visit the HuggingFace documentation or community forums.
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: Interactive STEM adventure game guidehhhhhhhhhhhhhhh
12
  ---
13
 
14
  # STEM Adventure Games
15
 
16
+ Interactive STEM adventure game guidehhhhhhhhhhhhhhh
17
 
18
  ## Quick Setup
19
 
 
31
  5. This enables automatic configuration updates
32
 
33
 
34
+ ### Step 3: Set Access Code
35
+ 1. In Settings β†’ Variables and secrets
36
+ 2. Add secret: `ACCESS_CODE`
37
+ 3. Set your chosen password
38
+ 4. Share with authorized users
39
+
40
 
41
  ### Step 3: Test Your Space
42
  Your Space should now be running! Try the example prompts or ask your own questions.
 
45
  - **Model**: google/gemini-2.0-flash-001
46
  - **API Key Variable**: API_KEY
47
  - **HF Token Variable**: HF_TOKEN (for auto-updates)
48
+ - **Access Control**: Enabled (ACCESS_CODE)
49
 
50
  ## Support
51
  For help, visit the HuggingFace documentation or community forums.
app.py CHANGED
@@ -13,18 +13,18 @@ from typing import List, Dict, Optional, Any, Tuple
13
 
14
  # Configuration
15
  SPACE_NAME = 'STEM Adventure Games'
16
- SPACE_DESCRIPTION = 'Interactive STEM adventure game guide'
17
 
18
  # Default configuration values
19
  DEFAULT_CONFIG = {
20
  'name': SPACE_NAME,
21
  'description': SPACE_DESCRIPTION,
22
- 'system_prompt': 'Transform into an interactive co-learning guide who creates Choose Your Own STEM Adventure games featuring historically significant scientific experiments, opening each session with an eye-catching unicode-styled arcade menu presenting 5-6 adventures drawn from Wikipedia\'s List of Experiments with brief enticing descriptions, then immersing players in vivid historical moments written in second person (e.g. \'You are Galileo Galilei\') that establish the year, location, prevailing beliefs, and tensions between established wisdom and emerging observations, presenting 3-4 numbered decision points per stage that reflect authentic scientific choices ranging from experimental design and measurement approaches to strategic decisions about convincing skeptics or timing publications, with each choice meaningfully different and leading to distinct paths forward, then narrating results with sensory details, colleague reactions, and unexpected observations that teach concepts naturally through unfolding drama rather than lectures, always ending scenes with new branching choices that maintain narrative momentum while reinforcing science as an iterative process of hypothesis, testing, and refinement, offering backtracking options to emphasize how so-called failed experiments provide crucial insights, balancing historical accuracy with engaging gameplay that shows how systematic thinking and creative problem-solving combine in scientific breakthroughs.',
23
  'temperature': 0.6,
24
  'max_tokens': 1000,
25
  'model': 'google/gemini-2.0-flash-001',
26
  'api_key_var': 'API_KEY',
27
- 'theme': 'Soft',
28
  'grounding_urls': ["https://en.wikipedia.org/wiki/List_of_experiments", "https://en.wikipedia.org/wiki/Scientific_method", "https://en.wikipedia.org/wiki/List_of_experiments#Biology", "https://en.wikipedia.org/wiki/List_of_experiments#Astronomy", "https://en.wikipedia.org/wiki/List_of_experiments#Chemistry", "https://en.wikipedia.org/wiki/List_of_experiments#Physics", "https://en.wikipedia.org/wiki/List_of_experiments#Geology"],
29
  'enable_dynamic_urls': True,
30
  'enable_file_upload': True,
@@ -113,35 +113,39 @@ class ConfigurationManager:
113
  os.remove(os.path.join(self.backup_dir, old_backup))
114
  except Exception as e:
115
  print(f"⚠️ Error cleaning up backups: {e}")
 
 
 
 
 
 
116
 
117
 
118
- # Global configuration instance
119
  config_manager = ConfigurationManager()
120
  config = config_manager.load()
121
 
122
- # Extract configuration values
123
  SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
124
  SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
125
  SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
 
 
126
  MODEL = config.get('model', DEFAULT_CONFIG['model'])
127
- API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
128
  THEME = config.get('theme', DEFAULT_CONFIG['theme'])
129
  GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
130
  ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
131
- ENABLE_FILE_UPLOAD = config.get('enable_file_upload', DEFAULT_CONFIG['enable_file_upload'])
132
- ACCESS_CODE = os.environ.get('ACCESS_CODE', '')
133
- API_KEY = os.environ.get(API_KEY_VAR, '')
 
 
 
134
  HF_TOKEN = os.environ.get('HF_TOKEN', '')
135
  SPACE_ID = os.environ.get('SPACE_ID', '')
136
 
137
- # Get theme instance
138
- theme = AVAILABLE_THEMES.get(THEME, gr.themes.Default())
139
-
140
- # Configuration defaults from loaded config
141
- temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
142
- max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
143
-
144
 
 
145
  def validate_api_key() -> bool:
146
  """Validate API key configuration"""
147
  if not API_KEY:
@@ -229,36 +233,28 @@ def extract_urls_from_text(text: str) -> List[str]:
229
 
230
 
231
  def process_file_upload(file_path: str) -> str:
232
- """Process uploaded file and extract text content"""
 
 
 
233
  try:
234
- if not os.path.exists(file_path):
235
- return "❌ File not found"
236
-
237
  file_size = os.path.getsize(file_path)
238
  file_name = os.path.basename(file_path)
239
- _, ext = os.path.splitext(file_name)
240
- ext = ext.lower()
241
 
242
- # Text files
243
- if ext in {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.csv', '.xml', '.yaml', '.yml'}:
244
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
245
- content = f.read(8000) # Limit to 8KB
246
- if len(content) == 8000:
247
- content += "\n... [truncated]"
248
- return f"πŸ“„ **{file_name}** ({file_size:,} bytes)\n```{ext[1:]}\n{content}\n```"
 
249
 
250
- # Code files
251
- elif ext in {'.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.rb', '.go', '.rs', '.swift', '.kt'}:
252
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
253
- content = f.read(8000)
254
- if len(content) == 8000:
255
- content += "\n... [truncated]"
256
- return f"πŸ“„ **{file_name}** ({file_size:,} bytes)\n```{ext[1:]}\n{content}\n```"
257
 
258
- # Config files
259
- elif ext in {'.ini', '.conf', '.toml', '.env'}:
260
- max_chars = 5000
261
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
262
  content = f.read(max_chars)
263
  if len(content) == max_chars:
264
  content += "\n... [truncated]"
@@ -328,21 +324,23 @@ Model: {MODEL}
328
  """
329
 
330
  message_count = 0
331
- for msg in history:
332
- role = msg.get('role', 'unknown')
333
- content = msg.get('content', '')
334
- message_count += 1
335
-
336
- if role == 'user':
337
- markdown_content += f"### πŸ§‘ User Message {message_count}\n{content}\n\n"
338
- elif role == 'assistant':
339
- markdown_content += f"### πŸ€– Assistant Response {message_count}\n{content}\n\n"
 
340
 
341
  return markdown_content
342
 
343
 
344
- def generate_response(message: str, history: List[Dict[str, str]], files: Optional[List[str]] = None) -> str:
345
- """Generate AI response"""
 
346
  # API key validation
347
  if not API_KEY:
348
  return f"""πŸ”‘ **API Key Required**
@@ -402,10 +400,11 @@ Get your API key at: https://openrouter.ai/keys"""
402
 
403
  # Add conversation history
404
  for msg in history:
405
- messages.append({
406
- "role": msg['role'],
407
- "content": msg['content']
408
- })
 
409
 
410
  # Add current message with context
411
  full_message = message
@@ -419,6 +418,7 @@ Get your API key at: https://openrouter.ai/keys"""
419
  "content": full_message
420
  })
421
 
 
422
  try:
423
  # Make API request
424
  headers = {
@@ -458,11 +458,17 @@ Get your API key at: https://openrouter.ai/keys"""
458
  return f"❌ API Error ({response.status_code}): {error_message}"
459
 
460
  except requests.exceptions.Timeout:
461
- return "⏱️ Request timed out. Please try again."
 
 
462
  except Exception as e:
463
  return f"❌ Error: {str(e)}"
464
 
465
 
 
 
 
 
466
  def verify_hf_token_access() -> Tuple[bool, str]:
467
  """Verify HuggingFace token and access"""
468
  if not HF_TOKEN:
@@ -486,199 +492,204 @@ def verify_hf_token_access() -> Tuple[bool, str]:
486
  return False, f"Error verifying HF token: {str(e)}"
487
 
488
 
489
- # Access control
490
- class AccessControl:
491
- """Handle access code verification"""
492
- def __init__(self):
493
- self._granted = not bool(ACCESS_CODE)
494
-
495
- def verify(self, code: str) -> bool:
496
- """Verify access code"""
497
- if not ACCESS_CODE:
498
- return True
499
- self._granted = (code == ACCESS_CODE)
500
- return self._granted
501
 
502
- def is_granted(self) -> bool:
503
- """Check if access is granted"""
504
- return self._granted
505
-
506
-
507
- access_control = AccessControl()
508
-
509
-
510
- def store_and_generate_response(message, history, *args):
511
- """Store history and generate response"""
512
- # Convert history to expected format
513
- formatted_history = []
514
- for h in history:
515
- if isinstance(h, dict):
516
- formatted_history.append(h)
517
- elif isinstance(h, (list, tuple)) and len(h) == 2:
518
- formatted_history.append({"role": "user", "content": h[0]})
519
- if h[1]:
520
- formatted_history.append({"role": "assistant", "content": h[1]})
521
 
522
- # Handle file uploads if enabled
523
- files = None
524
- if ENABLE_FILE_UPLOAD and args:
525
- files = args[0]
526
-
527
- response = generate_response(message, formatted_history, files)
528
-
529
- # Update stored history
530
- new_history = formatted_history + [
531
- {"role": "user", "content": message},
532
- {"role": "assistant", "content": response}
533
- ]
534
-
535
- return response
536
-
537
-
538
- def create_interface() -> gr.Blocks:
539
- """Create the main Gradio interface"""
540
- # API key validation
541
  API_KEY_VALID = validate_api_key()
542
 
543
  # Check HuggingFace access
544
  HF_ACCESS_VALID, HF_ACCESS_MESSAGE = verify_hf_token_access()
545
 
 
 
 
546
  with gr.Blocks(title=SPACE_NAME, theme=theme) as demo:
547
- # Use tabs for better organization
548
- with gr.Tabs() as main_tabs:
549
- # Chat Tab
550
- with gr.Tab("πŸ’¬ Chat"):
551
- gr.Markdown(f"# {SPACE_NAME}")
552
- gr.Markdown(SPACE_DESCRIPTION)
553
-
554
- # Conditional rendering for access control
555
- @gr.render(inputs=[])
556
- def render_chat_interface():
557
- if ACCESS_CODE and not access_control.is_granted():
558
- # Show access code input
559
- gr.Markdown("### πŸ” Access Required")
560
- gr.Markdown("This space requires an access code.")
561
-
562
- with gr.Row():
563
- access_input = gr.Textbox(
564
- label="Access Code",
565
- type="password",
566
- placeholder="Enter access code...",
567
- scale=3
568
- )
569
- access_btn = gr.Button("Submit", variant="primary", scale=1)
570
-
571
- access_status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
 
573
- def check_access(code):
574
- if access_control.verify(code):
575
- return "βœ… Access granted! Please refresh the page."
576
- else:
577
- return "❌ Invalid access code."
578
 
579
- access_btn.click(
580
- check_access,
581
- inputs=[access_input],
582
- outputs=[access_status]
583
- )
584
 
585
- access_input.submit(
586
- check_access,
587
- inputs=[access_input],
588
- outputs=[access_status]
589
- )
590
- else:
591
- # Show main chat interface
592
- # Process examples
593
- examples = config.get('examples', [])
594
- if isinstance(examples, str):
595
- try:
596
- examples = json.loads(examples)
597
- except:
598
- examples = []
599
 
600
- # Format examples for additional_inputs
601
- formatted_examples = None
602
- if examples and ENABLE_FILE_UPLOAD:
603
- formatted_examples = [[example, None] for example in examples]
604
- elif examples:
605
- formatted_examples = examples
606
 
607
- # Create additional inputs if file upload is enabled
608
- additional_inputs = None
609
- if ENABLE_FILE_UPLOAD:
610
- additional_inputs = [
611
- gr.File(
612
- label="πŸ“Ž Upload Files",
613
- file_count="multiple",
614
- file_types=["text", "image", "pdf"],
615
- visible=True
616
- )
617
- ]
618
 
619
- # Create chat interface
620
- chat_interface = gr.ChatInterface(
621
- fn=store_and_generate_response,
622
- title="",
623
- description="",
624
- examples=formatted_examples,
625
- type="messages",
626
- additional_inputs=additional_inputs if additional_inputs else None,
627
- submit_btn="Send"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  )
 
 
 
 
 
629
 
630
- # Export functionality using DownloadButton
631
- with gr.Row():
632
- export_btn = gr.DownloadButton(
633
- "πŸ“₯ Export Conversation",
634
- variant="secondary",
635
- size="sm"
636
- )
637
 
638
- # Track current history for export
639
- current_history = gr.State([])
 
 
640
 
641
- # Export handler
642
- def prepare_export(history):
643
- """Prepare conversation for export"""
644
- if not history:
645
- return None
646
-
647
- content = export_conversation_to_markdown(current_history.value or history)
648
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
649
- filename = f"conversation_{timestamp}.md"
650
-
651
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md') as f:
652
- f.write(content)
653
- return f.name
654
 
655
- export_btn.click(
656
- prepare_export,
657
- outputs=[export_btn]
 
 
 
 
 
 
 
 
 
 
658
  )
659
-
660
- # Configuration status
661
- with gr.Accordion("ℹ️ Configuration", open=False):
662
- config_info = f"""**Model:** {MODEL}
663
- **Temperature:** {temperature}
664
- **Max Tokens:** {max_tokens}
665
- **Theme:** {THEME}
666
- **File Upload:** {'Enabled' if ENABLE_FILE_UPLOAD else 'Disabled'}
667
- **Dynamic URLs:** {'Enabled' if ENABLE_DYNAMIC_URLS else 'Disabled'}"""
668
-
669
- if GROUNDING_URLS:
670
- config_info += f"\n**Grounding URLs:** {len(GROUNDING_URLS)} configured"
671
-
672
- if not API_KEY_VALID:
673
- config_info += f"\n\n⚠️ **Note:** API key ({API_KEY_VAR}) not configured"
674
-
675
- gr.Markdown(config_info)
676
-
677
- # Configuration Tab (if HF access is valid)
678
- if HF_ACCESS_VALID:
679
  with gr.Tab("βš™οΈ Configuration"):
680
  gr.Markdown("## Configuration Management")
681
- gr.Markdown(f"βœ… {HF_ACCESS_MESSAGE}")
 
 
 
 
 
 
 
682
 
683
  # Current configuration display
684
  with gr.Accordion("Current Configuration", open=False):
@@ -689,28 +700,34 @@ def create_interface() -> gr.Blocks:
689
  )
690
 
691
  # Configuration editor
692
- with gr.Group():
693
- gr.Markdown("### βš™οΈ Configuration Editor")
694
-
695
- # Basic settings
696
- with gr.Row():
697
- edit_name = gr.Textbox(
698
- label="Space Name",
699
- value=config.get('name', ''),
700
- max_lines=1
701
- )
702
- edit_model = gr.Dropdown(
703
- label="Model",
704
- choices=[
705
- "google/gemini-2.0-flash-001",
706
- "anthropic/claude-3.5-sonnet",
707
- "anthropic/claude-3.5-haiku",
708
- "openai/gpt-4o-mini",
709
- "meta-llama/llama-3.1-70b-instruct",
710
- "custom"
711
- ],
712
- value=config.get('model', '')
713
- )
 
 
 
 
 
 
714
 
715
  edit_description = gr.Textbox(
716
  label="Description",
@@ -724,7 +741,6 @@ def create_interface() -> gr.Blocks:
724
  lines=5
725
  )
726
 
727
- # Advanced settings
728
  with gr.Row():
729
  edit_temperature = gr.Slider(
730
  label="Temperature",
@@ -747,96 +763,127 @@ def create_interface() -> gr.Blocks:
747
  lines=3
748
  )
749
 
750
- # Action buttons
751
  with gr.Row():
752
  save_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
753
  reset_btn = gr.Button("↩️ Reset to Defaults", variant="secondary")
754
 
755
- config_status = gr.Markdown("")
756
-
757
- def save_configuration(name, description, system_prompt, model, temp, tokens, examples):
758
- """Save updated configuration"""
759
- try:
760
- updated_config = config.copy()
761
- updated_config.update({
762
- 'name': name,
763
- 'description': description,
764
- 'system_prompt': system_prompt,
765
- 'model': model,
766
- 'temperature': temp,
767
- 'max_tokens': int(tokens),
768
- 'examples': [ex.strip() for ex in examples.split('\n') if ex.strip()],
769
- 'locked': config.get('locked', False)
770
- })
771
-
772
- if config_manager.save(updated_config):
773
- # Auto-commit if HF token is available
774
- if HF_TOKEN and SPACE_ID:
775
- try:
776
- from huggingface_hub import HfApi
777
- api = HfApi()
778
- api.upload_file(
779
- path_or_fileobj=config_manager.config_path,
780
- path_in_repo="config.json",
781
- repo_id=SPACE_ID,
782
- repo_type="space",
783
- token=HF_TOKEN,
784
- commit_message="Update configuration via web UI"
785
- )
786
- return "βœ… Configuration saved and committed to repository!"
787
- except Exception as e:
788
- return f"βœ… Configuration saved locally. ⚠️ Auto-commit failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
789
  else:
790
- return "βœ… Configuration saved locally (no HF token for auto-commit)"
791
- else:
792
- return "❌ Failed to save configuration"
793
- except Exception as e:
794
- return f"❌ Error: {str(e)}"
795
-
796
- def reset_configuration():
797
- """Reset to default configuration"""
798
- try:
799
- if config_manager.save(DEFAULT_CONFIG):
800
- return (
801
- DEFAULT_CONFIG['name'],
802
- DEFAULT_CONFIG['description'],
803
- DEFAULT_CONFIG['system_prompt'],
804
- DEFAULT_CONFIG['model'],
805
- DEFAULT_CONFIG['temperature'],
806
- DEFAULT_CONFIG['max_tokens'],
807
- '\n'.join(DEFAULT_CONFIG['examples']),
808
- "βœ… Reset to default configuration"
809
- )
810
- else:
811
- return (*[gr.update() for _ in range(7)], "❌ Failed to reset")
812
- except Exception as e:
813
- return (*[gr.update() for _ in range(7)], f"❌ Error: {str(e)}")
814
-
815
- save_btn.click(
816
- save_configuration,
817
- inputs=[edit_name, edit_description, edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples],
818
- outputs=[config_status]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  )
820
-
821
- reset_btn.click(
822
- reset_configuration,
823
- outputs=[edit_name, edit_description, edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples, config_status]
 
 
824
  )
825
-
826
- # Show config lock status
827
- if config.get('locked', False):
828
- gr.Markdown("πŸ”’ **Configuration is locked**")
829
-
830
- return demo
 
 
 
 
 
 
 
 
831
 
832
 
833
- # Main execution
834
  if __name__ == "__main__":
835
- print("===== Application Startup at", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "=====")
836
  demo = create_interface()
837
- demo.launch(
838
- share=False,
839
- server_name="0.0.0.0",
840
- server_port=7860,
841
- show_error=True
842
- )
 
13
 
14
  # Configuration
15
  SPACE_NAME = 'STEM Adventure Games'
16
+ SPACE_DESCRIPTION = 'Interactive STEM adventure game guidehhhhhhhhhhhhhhh'
17
 
18
  # Default configuration values
19
  DEFAULT_CONFIG = {
20
  'name': SPACE_NAME,
21
  'description': SPACE_DESCRIPTION,
22
+ 'system_prompt': "Simulate an interactive game-based learning experience through Choose Your Own STEM Adventure games featuring historically significant scientific experiments. Open each session with an unicode-styled arcade menu that reads 'STEM_ADV_GAMES` one stacked on top of the other. Underneath, frame the game in two sentences and widely sample 3-4 optional adventures from Wikipedia's List of Experiments. In the first stage, be brief and incrementally build more narrative content into the chat, immersing players in vivid historical moments written in second person (e.g. 'You are Galileo Galilei') that establish the year, location, prevailing beliefs, and tensions between established wisdom and emerging observations. Each section has 4 numbered decision points per stage that reflect experimental choices that subtly demonstrate a facet of the scientific method, with each choice meaningfully distinct in affording paths forward. Always end scenes with new branching choices that build a narrative progression based on concrete experimental procedures in laboratory environments. Offer backtracking options to emphasize how so-called failed experiments provide crucial insights, balancing historical accuracy with engaging gameplay that shows how systematic thinking and creative problem-solving combine in scientific breakthroughs. Goal: Teach scientific thinking through interactive historical moments where students make experimental design decisions.",
23
  'temperature': 0.6,
24
  'max_tokens': 1000,
25
  'model': 'google/gemini-2.0-flash-001',
26
  'api_key_var': 'API_KEY',
27
+ 'theme': 'Glass',
28
  'grounding_urls': ["https://en.wikipedia.org/wiki/List_of_experiments", "https://en.wikipedia.org/wiki/Scientific_method", "https://en.wikipedia.org/wiki/List_of_experiments#Biology", "https://en.wikipedia.org/wiki/List_of_experiments#Astronomy", "https://en.wikipedia.org/wiki/List_of_experiments#Chemistry", "https://en.wikipedia.org/wiki/List_of_experiments#Physics", "https://en.wikipedia.org/wiki/List_of_experiments#Geology"],
29
  'enable_dynamic_urls': True,
30
  'enable_file_upload': True,
 
113
  os.remove(os.path.join(self.backup_dir, old_backup))
114
  except Exception as e:
115
  print(f"⚠️ Error cleaning up backups: {e}")
116
+
117
+ def get(self, key: str, default: Any = None) -> Any:
118
+ """Get configuration value"""
119
+ if self._config is None:
120
+ self.load()
121
+ return self._config.get(key, default)
122
 
123
 
124
+ # Initialize configuration manager
125
  config_manager = ConfigurationManager()
126
  config = config_manager.load()
127
 
128
+ # Load configuration values
129
  SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
130
  SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
131
  SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
132
+ temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
133
+ max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
134
  MODEL = config.get('model', DEFAULT_CONFIG['model'])
 
135
  THEME = config.get('theme', DEFAULT_CONFIG['theme'])
136
  GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
137
  ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
138
+ ENABLE_FILE_UPLOAD = config.get('enable_file_upload', DEFAULT_CONFIG.get('enable_file_upload', True))
139
+
140
+ # Environment variables
141
+ ACCESS_CODE = os.environ.get("ACCESS_CODE")
142
+ API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
143
+ API_KEY = os.environ.get(API_KEY_VAR, "").strip() or None
144
  HF_TOKEN = os.environ.get('HF_TOKEN', '')
145
  SPACE_ID = os.environ.get('SPACE_ID', '')
146
 
 
 
 
 
 
 
 
147
 
148
+ # Utility functions
149
  def validate_api_key() -> bool:
150
  """Validate API key configuration"""
151
  if not API_KEY:
 
233
 
234
 
235
  def process_file_upload(file_path: str) -> str:
236
+ """Process uploaded file with Gradio best practices"""
237
+ if not file_path or not os.path.exists(file_path):
238
+ return "❌ File not found"
239
+
240
  try:
 
 
 
241
  file_size = os.path.getsize(file_path)
242
  file_name = os.path.basename(file_path)
243
+ _, ext = os.path.splitext(file_path.lower())
 
244
 
245
+ # Text file extensions
246
+ text_extensions = {
247
+ '.txt', '.md', '.markdown', '.rst',
248
+ '.py', '.js', '.jsx', '.ts', '.tsx', '.json', '.yaml', '.yml',
249
+ '.html', '.htm', '.xml', '.css', '.scss',
250
+ '.java', '.c', '.cpp', '.h', '.cs', '.go', '.rs',
251
+ '.sh', '.bash', '.log', '.csv', '.sql'
252
+ }
253
 
254
+ max_chars = 5000 # Define max_chars limit for file reading
 
 
 
 
 
 
255
 
256
+ if ext in text_extensions:
257
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
 
 
258
  content = f.read(max_chars)
259
  if len(content) == max_chars:
260
  content += "\n... [truncated]"
 
324
  """
325
 
326
  message_count = 0
327
+ for message in history:
328
+ if isinstance(message, dict):
329
+ role = message.get('role', 'unknown')
330
+ content = message.get('content', '')
331
+
332
+ if role == 'user':
333
+ message_count += 1
334
+ markdown_content += f"## User Message {message_count}\n\n{content}\n\n"
335
+ elif role == 'assistant':
336
+ markdown_content += f"## Assistant Response {message_count}\n\n{content}\n\n---\n\n"
337
 
338
  return markdown_content
339
 
340
 
341
+ def generate_response(message: str, history: List[Dict[str, str]], files: Optional[List] = None) -> str:
342
+ """Generate response using OpenRouter API with file support"""
343
+
344
  # API key validation
345
  if not API_KEY:
346
  return f"""πŸ”‘ **API Key Required**
 
400
 
401
  # Add conversation history
402
  for msg in history:
403
+ if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
404
+ messages.append({
405
+ "role": msg['role'],
406
+ "content": msg['content']
407
+ })
408
 
409
  # Add current message with context
410
  full_message = message
 
418
  "content": full_message
419
  })
420
 
421
+ # Make API request
422
  try:
423
  # Make API request
424
  headers = {
 
458
  return f"❌ API Error ({response.status_code}): {error_message}"
459
 
460
  except requests.exceptions.Timeout:
461
+ return "⏰ Request timeout (30s limit). Try a shorter message or different model."
462
+ except requests.exceptions.ConnectionError:
463
+ return "🌐 Connection error. Check your internet connection and try again."
464
  except Exception as e:
465
  return f"❌ Error: {str(e)}"
466
 
467
 
468
+ # Chat history for export
469
+ chat_history_store = []
470
+
471
+
472
  def verify_hf_token_access() -> Tuple[bool, str]:
473
  """Verify HuggingFace token and access"""
474
  if not HF_TOKEN:
 
492
  return False, f"Error verifying HF token: {str(e)}"
493
 
494
 
495
+ # Create main interface with clean tab structure
496
+ def create_interface():
497
+ """Create the Gradio interface with clean tab structure"""
 
 
 
 
 
 
 
 
 
498
 
499
+ # Get theme
500
+ theme = AVAILABLE_THEMES.get(THEME, gr.themes.Default())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ # Validate API key on startup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  API_KEY_VALID = validate_api_key()
504
 
505
  # Check HuggingFace access
506
  HF_ACCESS_VALID, HF_ACCESS_MESSAGE = verify_hf_token_access()
507
 
508
+ # Access control check
509
+ has_access = ACCESS_CODE is None # No access code required
510
+
511
  with gr.Blocks(title=SPACE_NAME, theme=theme) as demo:
512
+ # State for access control
513
+ access_granted = gr.State(has_access)
514
+
515
+ # Header - always visible
516
+ gr.Markdown(f"# {SPACE_NAME}")
517
+ gr.Markdown(SPACE_DESCRIPTION)
518
+
519
+ # Access control panel (visible when access not granted)
520
+ with gr.Column(visible=(not has_access)) as access_panel:
521
+ gr.Markdown("### πŸ” Access Required")
522
+ gr.Markdown("Please enter the access code:")
523
+
524
+ with gr.Row():
525
+ access_input = gr.Textbox(
526
+ label="Access Code",
527
+ placeholder="Enter access code...",
528
+ type="password",
529
+ scale=3
530
+ )
531
+ access_btn = gr.Button("Submit", variant="primary", scale=1)
532
+
533
+ access_status = gr.Markdown()
534
+
535
+ # Main interface (visible when access granted)
536
+ with gr.Column(visible=has_access) as main_panel:
537
+ with gr.Tabs() as tabs:
538
+ # Chat Tab
539
+ with gr.Tab("πŸ’¬ Chat"):
540
+ # Get examples
541
+ examples = config.get('examples', [])
542
+ if isinstance(examples, str):
543
+ try:
544
+ examples = json.loads(examples)
545
+ except:
546
+ examples = []
547
+
548
+ # State to hold uploaded files
549
+ uploaded_files = gr.State([])
550
+
551
+ # Create chat interface
552
+ chatbot = gr.Chatbot(type="messages", height=400)
553
+ msg = gr.Textbox(label="Message", placeholder="Type your message here...", lines=2)
554
+
555
+ # Examples section
556
+ if examples:
557
+ gr.Examples(examples=examples, inputs=msg)
558
+
559
+ with gr.Row():
560
+ submit_btn = gr.Button("Send", variant="primary")
561
+ clear_btn = gr.Button("Clear")
562
+
563
+ # Chat functionality
564
+ def respond(message, chat_history, files_state, is_granted):
565
+ if not is_granted:
566
+ return chat_history, "", is_granted
567
 
568
+ if not message:
569
+ return chat_history, "", is_granted
 
 
 
570
 
571
+ # Format history for the generate_response function
572
+ formatted_history = []
573
+ for h in chat_history:
574
+ if isinstance(h, dict):
575
+ formatted_history.append(h)
576
 
577
+ # Get response
578
+ response = generate_response(message, formatted_history, files_state)
 
 
 
 
 
 
 
 
 
 
 
 
579
 
580
+ # Update chat history
581
+ chat_history = chat_history + [
582
+ {"role": "user", "content": message},
583
+ {"role": "assistant", "content": response}
584
+ ]
 
585
 
586
+ # Update stored history for export
587
+ global chat_history_store
588
+ chat_history_store = chat_history
 
 
 
 
 
 
 
 
589
 
590
+ return chat_history, "", is_granted
591
+
592
+ # Wire up the interface
593
+ msg.submit(respond, [msg, chatbot, uploaded_files, access_granted], [chatbot, msg, access_granted])
594
+ submit_btn.click(respond, [msg, chatbot, uploaded_files, access_granted], [chatbot, msg, access_granted])
595
+ clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
596
+
597
+ # File upload accordion
598
+ if ENABLE_FILE_UPLOAD:
599
+ with gr.Accordion("πŸ“Ž Upload Files", open=False):
600
+ file_upload = gr.File(
601
+ label="Upload Files",
602
+ file_types=None,
603
+ file_count="multiple",
604
+ visible=True,
605
+ interactive=True
606
+ )
607
+ clear_files_btn = gr.Button("Clear Files", size="sm", variant="secondary")
608
+ uploaded_files_display = gr.Markdown("", visible=False)
609
+
610
+ def handle_file_upload(files):
611
+ if not files:
612
+ return [], "", gr.update(visible=False)
613
+
614
+ file_names = []
615
+ for file_info in files:
616
+ if isinstance(file_info, dict):
617
+ file_path = file_info.get('path', file_info.get('name', ''))
618
+ else:
619
+ file_path = str(file_info)
620
+
621
+ if file_path and os.path.exists(file_path):
622
+ file_names.append(os.path.basename(file_path))
623
+
624
+ if file_names:
625
+ display_text = f"πŸ“Ž **Uploaded files:** {', '.join(file_names)}"
626
+ return files, display_text, gr.update(visible=True)
627
+ return [], "", gr.update(visible=False)
628
+
629
+ def clear_files():
630
+ return None, [], "", gr.update(visible=False)
631
+
632
+ file_upload.change(
633
+ handle_file_upload,
634
+ inputs=[file_upload],
635
+ outputs=[uploaded_files, uploaded_files_display, uploaded_files_display]
636
+ )
637
+
638
+ clear_files_btn.click(
639
+ clear_files,
640
+ outputs=[file_upload, uploaded_files, uploaded_files_display, uploaded_files_display]
641
+ )
642
+
643
+ # Export functionality
644
+ with gr.Row():
645
+ export_btn = gr.DownloadButton(
646
+ "πŸ“₯ Export Conversation",
647
+ variant="secondary",
648
+ size="sm"
649
  )
650
+
651
+ # Export handler
652
+ def prepare_export():
653
+ if not chat_history_store:
654
+ return None
655
 
656
+ content = export_conversation_to_markdown(chat_history_store)
 
 
 
 
 
 
657
 
658
+ # Create filename
659
+ space_name_safe = re.sub(r'[^a-zA-Z0-9]+', '_', SPACE_NAME).lower()
660
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
661
+ filename = f"{space_name_safe}_conversation_{timestamp}.md"
662
 
663
+ # Save to temp file
664
+ temp_path = Path(tempfile.gettempdir()) / filename
665
+ temp_path.write_text(content, encoding='utf-8')
 
 
 
 
 
 
 
 
 
 
666
 
667
+ return str(temp_path)
668
+
669
+ export_btn.click(
670
+ prepare_export,
671
+ outputs=[export_btn]
672
+ )
673
+
674
+ # Configuration accordion
675
+ with gr.Accordion("ℹ️ Configuration", open=False):
676
+ gr.JSON(
677
+ value=config,
678
+ label="config.json",
679
+ show_label=True
680
  )
681
+
682
+ # Configuration Tab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  with gr.Tab("βš™οΈ Configuration"):
684
  gr.Markdown("## Configuration Management")
685
+
686
+ # Show authentication status
687
+ if HF_ACCESS_VALID:
688
+ gr.Markdown(f"βœ… {HF_ACCESS_MESSAGE}")
689
+ gr.Markdown("Configuration changes will be saved to the HuggingFace repository.")
690
+ else:
691
+ gr.Markdown(f"ℹ️ {HF_ACCESS_MESSAGE}")
692
+ gr.Markdown("Set HF_TOKEN in Space secrets to enable auto-save.")
693
 
694
  # Current configuration display
695
  with gr.Accordion("Current Configuration", open=False):
 
700
  )
701
 
702
  # Configuration editor
703
+ gr.Markdown("### βš™οΈ Configuration Editor")
704
+
705
+ # Show lock status if locked
706
+ if config.get('locked', False):
707
+ gr.Markdown("⚠️ **Note:** Configuration is locked. Contact an administrator to unlock.")
708
+
709
+ # Basic settings
710
+ with gr.Column():
711
+ edit_name = gr.Textbox(
712
+ label="Space Name",
713
+ value=config.get('name', ''),
714
+ max_lines=1
715
+ )
716
+ edit_model = gr.Dropdown(
717
+ label="Model",
718
+ choices=[
719
+ "google/gemini-2.0-flash-001",
720
+ "anthropic/claude-3.5-haiku",
721
+ "openai/gpt-4o-mini",
722
+ "openai/gpt-4o-mini-search-preview",
723
+ "meta-llama/llama-3.1-70b-instruct",
724
+ "qwen/qwen-2.5-72b-instruct",
725
+ "google/gemma-3-27b-it",
726
+ "custom"
727
+ ],
728
+ value=config.get('model', ''),
729
+ allow_custom_value=True
730
+ )
731
 
732
  edit_description = gr.Textbox(
733
  label="Description",
 
741
  lines=5
742
  )
743
 
 
744
  with gr.Row():
745
  edit_temperature = gr.Slider(
746
  label="Temperature",
 
763
  lines=3
764
  )
765
 
766
+ # Configuration actions
767
  with gr.Row():
768
  save_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
769
  reset_btn = gr.Button("↩️ Reset to Defaults", variant="secondary")
770
 
771
+ config_status = gr.Markdown()
772
+
773
+ def save_configuration(name, description, system_prompt, model, temp, tokens, examples):
774
+ """Save updated configuration"""
775
+ try:
776
+ updated_config = config.copy()
777
+ updated_config.update({
778
+ 'name': name,
779
+ 'description': description,
780
+ 'system_prompt': system_prompt,
781
+ 'model': model,
782
+ 'temperature': temp,
783
+ 'max_tokens': int(tokens),
784
+ 'examples': [ex.strip() for ex in examples.split('\n') if ex.strip()],
785
+ 'locked': config.get('locked', False)
786
+ })
787
+
788
+ if config_manager.save(updated_config):
789
+ # Auto-commit if HF token is available
790
+ if HF_TOKEN and SPACE_ID:
791
+ try:
792
+ from huggingface_hub import HfApi, CommitOperationAdd
793
+ api = HfApi(token=HF_TOKEN)
794
+
795
+ operations = [
796
+ CommitOperationAdd(
797
+ path_or_fileobj=config_manager.config_path,
798
+ path_in_repo="config.json"
799
+ )
800
+ ]
801
+
802
+ api.create_commit(
803
+ repo_id=SPACE_ID,
804
+ operations=operations,
805
+ commit_message="Update configuration via web UI",
806
+ commit_description=f"Configuration update at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
807
+ repo_type="space",
808
+ token=HF_TOKEN
809
+ )
810
+ return "βœ… Configuration saved and committed to repository!"
811
+ except Exception as e:
812
+ return f"βœ… Configuration saved locally. ⚠️ Auto-commit failed: {str(e)}"
813
+ else:
814
+ return "βœ… Configuration saved locally (no HF token for auto-commit)"
815
  else:
816
+ return "❌ Failed to save configuration"
817
+
818
+ except Exception as e:
819
+ return f"❌ Error: {str(e)}"
820
+
821
+ save_btn.click(
822
+ save_configuration,
823
+ inputs=[edit_name, edit_description, edit_system_prompt, edit_model,
824
+ edit_temperature, edit_max_tokens, edit_examples],
825
+ outputs=[config_status]
826
+ )
827
+
828
+ def reset_configuration():
829
+ """Reset to default configuration"""
830
+ try:
831
+ if config_manager.save(DEFAULT_CONFIG):
832
+ return (
833
+ DEFAULT_CONFIG['name'],
834
+ DEFAULT_CONFIG['description'],
835
+ DEFAULT_CONFIG['system_prompt'],
836
+ DEFAULT_CONFIG['model'],
837
+ DEFAULT_CONFIG['temperature'],
838
+ DEFAULT_CONFIG['max_tokens'],
839
+ '\n'.join(DEFAULT_CONFIG['examples']),
840
+ "βœ… Reset to default configuration"
841
+ )
842
+ else:
843
+ return (*[gr.update() for _ in range(7)], "❌ Failed to reset")
844
+ except Exception as e:
845
+ return (*[gr.update() for _ in range(7)], f"❌ Error: {str(e)}")
846
+
847
+ reset_btn.click(
848
+ reset_configuration,
849
+ outputs=[edit_name, edit_description, edit_system_prompt, edit_model,
850
+ edit_temperature, edit_max_tokens, edit_examples, config_status]
851
+ )
852
+
853
+ # Access control handler
854
+ if ACCESS_CODE:
855
+ def handle_access(code, current_state):
856
+ if code == ACCESS_CODE:
857
+ return (
858
+ gr.update(visible=False), # Hide access panel
859
+ gr.update(visible=True), # Show main panel
860
+ gr.update(value="βœ… Access granted!"), # Status message
861
+ True # Update state
862
  )
863
+ else:
864
+ return (
865
+ gr.update(visible=True), # Keep access panel visible
866
+ gr.update(visible=False), # Keep main panel hidden
867
+ gr.update(value="❌ Invalid access code. Please try again."), # Status message
868
+ False # State remains false
869
  )
870
+
871
+ access_btn.click(
872
+ handle_access,
873
+ inputs=[access_input, access_granted],
874
+ outputs=[access_panel, main_panel, access_status, access_granted]
875
+ )
876
+
877
+ access_input.submit(
878
+ handle_access,
879
+ inputs=[access_input, access_granted],
880
+ outputs=[access_panel, main_panel, access_status, access_granted]
881
+ )
882
+
883
+ return demo
884
 
885
 
886
+ # Create and launch the interface
887
  if __name__ == "__main__":
 
888
  demo = create_interface()
889
+ demo.launch()
 
 
 
 
 
config.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "STEM Adventure Games",
3
- "description": "Interactive STEM adventure game guide",
4
- "system_prompt": "Transform into an interactive co-learning guide who creates Choose Your Own STEM Adventure games featuring historically significant scientific experiments, opening each session with an eye-catching unicode-styled arcade menu presenting 5-6 adventures drawn from Wikipedia's List of Experiments with brief enticing descriptions, then immersing players in vivid historical moments written in second person (e.g. 'You are Galileo Galilei') that establish the year, location, prevailing beliefs, and tensions between established wisdom and emerging observations, presenting 3-4 numbered decision points per stage that reflect authentic scientific choices ranging from experimental design and measurement approaches to strategic decisions about convincing skeptics or timing publications, with each choice meaningfully different and leading to distinct paths forward, then narrating results with sensory details, colleague reactions, and unexpected observations that teach concepts naturally through unfolding drama rather than lectures, always ending scenes with new branching choices that maintain narrative momentum while reinforcing science as an iterative process of hypothesis, testing, and refinement, offering backtracking options to emphasize how so-called failed experiments provide crucial insights, balancing historical accuracy with engaging gameplay that shows how systematic thinking and creative problem-solving combine in scientific breakthroughs.",
5
  "model": "google/gemini-2.0-flash-001",
6
  "api_key_var": "API_KEY",
7
  "temperature": 0.6,
@@ -22,5 +22,5 @@
22
  ],
23
  "enable_dynamic_urls": true,
24
  "enable_file_upload": true,
25
- "theme": "Default"
26
  }
 
1
  {
2
  "name": "STEM Adventure Games",
3
+ "description": "Interactive STEM adventure game guidehhhhhhhhhhhhhhh",
4
+ "system_prompt": "Simulate an interactive game-based learning experience through Choose Your Own STEM Adventure games featuring historically significant scientific experiments. Open each session with an unicode-styled arcade menu that reads 'STEM_ADV_GAMES` one stacked on top of the other. Underneath, frame the game in two sentences and widely sample 3-4 optional adventures from Wikipedia's List of Experiments. In the first stage, be brief and incrementally build more narrative content into the chat, immersing players in vivid historical moments written in second person (e.g. 'You are Galileo Galilei') that establish the year, location, prevailing beliefs, and tensions between established wisdom and emerging observations. Each section has 4 numbered decision points per stage that reflect experimental choices that subtly demonstrate a facet of the scientific method, with each choice meaningfully distinct in affording paths forward. Always end scenes with new branching choices that build a narrative progression based on concrete experimental procedures in laboratory environments. Offer backtracking options to emphasize how so-called failed experiments provide crucial insights, balancing historical accuracy with engaging gameplay that shows how systematic thinking and creative problem-solving combine in scientific breakthroughs. Goal: Teach scientific thinking through interactive historical moments where students make experimental design decisions.",
5
  "model": "google/gemini-2.0-flash-001",
6
  "api_key_var": "API_KEY",
7
  "temperature": 0.6,
 
22
  ],
23
  "enable_dynamic_urls": true,
24
  "enable_file_upload": true,
25
+ "theme": "Glass"
26
  }