diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,2194 +1,1004 @@ -import warnings -warnings.filterwarnings("ignore", message="The 'tuples' format for chatbot messages is deprecated") - -# Ensure we're using Gradio 4.x -import gradio as gr -print(f"Gradio version: {gr.__version__}") -import json -import zipfile -import io -import os -from datetime import datetime -from dotenv import load_dotenv -import requests -from bs4 import BeautifulSoup -import tempfile -from pathlib import Path -from support_docs import create_support_docs, export_conversation_to_markdown - -# Simple URL content fetching using requests and BeautifulSoup -def get_grounding_context_simple(urls): - """Fetch grounding context using enhanced HTTP requests""" - if not urls: - return "" - - context_parts = [] - for i, url in enumerate(urls, 1): - if url and url.strip(): - # Use enhanced URL extraction for any URLs within the URL text - extracted_urls = extract_urls_from_text(url.strip()) - target_url = extracted_urls[0] if extracted_urls else url.strip() - - content = enhanced_fetch_url_content(target_url) - context_parts.append(f"Context from URL {i} ({target_url}):\n{content}") - - if context_parts: - return "\n\n" + "\n\n".join(context_parts) + "\n\n" - return "" - - -# Load environment variables from .env file -load_dotenv() - -# Utility functions -import re - -def extract_urls_from_text(text): - """Extract URLs from text using regex with enhanced validation""" - url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+' - urls = re.findall(url_pattern, text) - - # Basic URL validation and cleanup - validated_urls = [] - for url in urls: - # Remove trailing punctuation that might be captured - url = url.rstrip('.,!?;:') - # Basic domain validation - if '.' in url and len(url) > 10: - validated_urls.append(url) - - return validated_urls - -def validate_url_domain(url): - """Basic URL domain validation""" - try: - from urllib.parse import urlparse - parsed = urlparse(url) - # Check for valid domain structure - if parsed.netloc and '.' in parsed.netloc: - return True - except: - pass - return False - -def enhanced_fetch_url_content(url, enable_search_validation=False): - """Enhanced URL content fetching with optional search validation""" - if not validate_url_domain(url): - return f"Invalid URL format: {url}" - - try: - # Enhanced headers for better compatibility - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive' - } - - response = requests.get(url, timeout=15, headers=headers) - response.raise_for_status() - soup = BeautifulSoup(response.content, 'html.parser') - - # Enhanced content cleaning - for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]): - element.decompose() - - # Extract main content preferentially - main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or soup - text = main_content.get_text() - - # Enhanced text cleaning - lines = (line.strip() for line in text.splitlines()) - chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) - text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2) - - # Smart truncation - try to end at sentence boundaries - if len(text) > 4000: - truncated = text[:4000] - last_period = truncated.rfind('.') - if last_period > 3000: # If we can find a reasonable sentence break - text = truncated[:last_period + 1] - else: - text = truncated + "..." - - return text if text.strip() else "No readable content found at this URL" - - except requests.exceptions.Timeout: - return f"Timeout error fetching {{url}} (15s limit exceeded)" - except requests.exceptions.RequestException as e: - return f"Error fetching {{url}}: {{str(e)}}" - except Exception as e: - return f"Error processing content from {{url}}: {{str(e)}}" - -# Template for generated space app (based on mvp_simple.py) -SPACE_TEMPLATE = '''import gradio as gr -import tempfile -import os -import requests -import json -import re -from bs4 import BeautifulSoup -from datetime import datetime -import urllib.parse - - -# Configuration -SPACE_NAME = "{name}" -SPACE_DESCRIPTION = "{description}" - -# Default configuration values -DEFAULT_SYSTEM_PROMPT = """{system_prompt}""" -DEFAULT_TEMPERATURE = {temperature} -DEFAULT_MAX_TOKENS = {max_tokens} - -# Try to load configuration from file (if modified by faculty) -try: - with open('config.json', 'r') as f: - saved_config = json.load(f) - SYSTEM_PROMPT = saved_config.get('system_prompt', DEFAULT_SYSTEM_PROMPT) - temperature = saved_config.get('temperature', DEFAULT_TEMPERATURE) - max_tokens = saved_config.get('max_tokens', DEFAULT_MAX_TOKENS) - print("✅ Loaded configuration from config.json") -except: - # Use defaults if no config file or error - SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT - temperature = DEFAULT_TEMPERATURE - max_tokens = DEFAULT_MAX_TOKENS - print("ℹ️ Using default configuration") - -MODEL = "{model}" -THEME = "{theme}" # Gradio theme name -GROUNDING_URLS = {grounding_urls} -# Get access code from environment variable for security -# If ACCESS_CODE is not set, no access control is applied -ACCESS_CODE = os.environ.get("ACCESS_CODE") -ENABLE_DYNAMIC_URLS = {enable_dynamic_urls} - -# Get API key from environment - customizable variable name with validation -API_KEY = os.environ.get("{api_key_var}") -if API_KEY: - API_KEY = API_KEY.strip() # Remove any whitespace - if not API_KEY: # Check if empty after stripping - API_KEY = None - -# API Key validation and logging -def validate_api_key(): - """Validate API key configuration with detailed logging""" - if not API_KEY: - print(f"⚠️ API KEY CONFIGURATION ERROR:") - print(f" Variable name: {api_key_var}") - print(f" Status: Not set or empty") - print(f" Action needed: Set '{api_key_var}' in HuggingFace Space secrets") - print(f" Expected format: sk-or-xxxxxxxxxx") - return False - elif not API_KEY.startswith('sk-or-'): - print(f"⚠️ API KEY FORMAT WARNING:") - print(f" Variable name: {api_key_var}") - print(f" Current value: {{API_KEY[:10]}}..." if len(API_KEY) > 10 else API_KEY) - print(f" Expected format: sk-or-xxxxxxxxxx") - print(f" Note: OpenRouter keys should start with 'sk-or-'") - return True # Still try to use it - else: - print(f"✅ API Key configured successfully") - print(f" Variable: {api_key_var}") - print(f" Format: Valid OpenRouter key") - return True - -# Validate on startup -try: - API_KEY_VALID = validate_api_key() -except NameError: - # During template generation, API_KEY might not be defined yet - API_KEY_VALID = False - -def validate_url_domain(url): - """Basic URL domain validation""" - try: - from urllib.parse import urlparse - parsed = urlparse(url) - # Check for valid domain structure - if parsed.netloc and '.' in parsed.netloc: - return True - except: - pass - return False - -def fetch_url_content(url): - """Enhanced URL content fetching with improved compatibility and error handling""" - if not validate_url_domain(url): - return f"Invalid URL format: {{url}}" - - try: - # Enhanced headers for better compatibility - headers = {{ - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive' - }} - - response = requests.get(url, timeout=15, headers=headers) - response.raise_for_status() - soup = BeautifulSoup(response.content, 'html.parser') - - # Enhanced content cleaning - for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]): - element.decompose() - - # Extract main content preferentially - main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or soup - text = main_content.get_text() - - # Enhanced text cleaning - lines = (line.strip() for line in text.splitlines()) - chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) - text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2) - - # Smart truncation - try to end at sentence boundaries - if len(text) > 4000: - truncated = text[:4000] - last_period = truncated.rfind('.') - if last_period > 3000: # If we can find a reasonable sentence break - text = truncated[:last_period + 1] - else: - text = truncated + "..." - - return text if text.strip() else "No readable content found at this URL" - - except requests.exceptions.Timeout: - return f"Timeout error fetching {{url}} (15s limit exceeded)" - except requests.exceptions.RequestException as e: - return f"Error fetching {{url}}: {{str(e)}}" - except Exception as e: - return f"Error processing content from {{url}}: {{str(e)}}" - -def extract_urls_from_text(text): - """Extract URLs from text using regex with enhanced validation""" - import re - url_pattern = r'https?://[^\\s<>"{{}}|\\\\^`\\[\\]"]+' - urls = re.findall(url_pattern, text) - - # Basic URL validation and cleanup - validated_urls = [] - for url in urls: - # Remove trailing punctuation that might be captured - url = url.rstrip('.,!?;:') - # Basic domain validation - if '.' in url and len(url) > 10: - validated_urls.append(url) - - return validated_urls - -# Global cache for URL content to avoid re-crawling in generated spaces -_url_content_cache = {{}} - -def get_grounding_context(): - """Fetch context from grounding URLs with caching""" - if not GROUNDING_URLS: - return "" - - # Create cache key from URLs - cache_key = tuple(sorted([url for url in GROUNDING_URLS if url and url.strip()])) - - # Check cache first - if cache_key in _url_content_cache: - return _url_content_cache[cache_key] - - context_parts = [] - for i, url in enumerate(GROUNDING_URLS, 1): - if url.strip(): - content = fetch_url_content(url.strip()) - # Add priority indicators - priority_label = "PRIMARY" if i <= 2 else "SECONDARY" - context_parts.append(f"[{{priority_label}}] Context from URL {{i}} ({{url}}):\\n{{content}}") - - if context_parts: - result = "\\n\\n" + "\\n\\n".join(context_parts) + "\\n\\n" - else: - result = "" - - # Cache the result - _url_content_cache[cache_key] = result - return result - -def export_conversation_to_markdown(conversation_history): - """Export conversation history to markdown format""" - if not conversation_history: - return "No conversation to export." - - markdown_content = f"""# Conversation Export -Generated on: {{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}} - ---- - -""" - - message_pair_count = 0 - for i, message in enumerate(conversation_history): - if isinstance(message, dict): - role = message.get('role', 'unknown') - content = message.get('content', '') - - if role == 'user': - message_pair_count += 1 - markdown_content += f"## User Message {{message_pair_count}}\\n\\n{{content}}\\n\\n" - elif role == 'assistant': - markdown_content += f"## Assistant Response {{message_pair_count}}\\n\\n{{content}}\\n\\n---\\n\\n" - elif isinstance(message, (list, tuple)) and len(message) >= 2: - # Handle legacy tuple format: ["user msg", "assistant msg"] - message_pair_count += 1 - user_msg, assistant_msg = message[0], message[1] - if user_msg: - markdown_content += f"## User Message {{message_pair_count}}\\n\\n{{user_msg}}\\n\\n" - if assistant_msg: - markdown_content += f"## Assistant Response {{message_pair_count}}\\n\\n{{assistant_msg}}\\n\\n---\\n\\n" - - return markdown_content - - -def generate_response(message, history): - """Generate response using OpenRouter API""" - - # Enhanced API key validation with helpful messages - if not API_KEY: - error_msg = f"🔑 **API Key Required**\\n\\n" - error_msg += f"Please configure your OpenRouter API key:\\n" - error_msg += f"1. Go to Settings (⚙️) in your HuggingFace Space\\n" - error_msg += f"2. Click 'Variables and secrets'\\n" - error_msg += f"3. Add secret: **{api_key_var}**\\n" - error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\\n\\n" - error_msg += f"Get your API key at: https://openrouter.ai/keys" - print(f"❌ API request failed: No API key configured for {api_key_var}") - return error_msg - - # Get grounding context - grounding_context = get_grounding_context() - - - # If dynamic URLs are enabled, check message for URLs to fetch - if ENABLE_DYNAMIC_URLS: - urls_in_message = extract_urls_from_text(message) - if urls_in_message: - # Fetch content from URLs mentioned in the message - dynamic_context_parts = [] - for url in urls_in_message[:3]: # Limit to 3 URLs per message - content = fetch_url_content(url) - dynamic_context_parts.append(f"\\n\\nDynamic context from {{url}}:\\n{{content}}") - if dynamic_context_parts: - grounding_context += "\\n".join(dynamic_context_parts) - - # Build enhanced system prompt with grounding context - enhanced_system_prompt = SYSTEM_PROMPT + grounding_context - - # Build messages array for the API - messages = [{{"role": "system", "content": enhanced_system_prompt}}] - - # Add conversation history - handle both modern messages format and legacy tuples - for chat in history: - if isinstance(chat, dict): - # Modern format: {{"role": "user", "content": "..."}} or {{"role": "assistant", "content": "..."}} - messages.append(chat) - elif isinstance(chat, (list, tuple)) and len(chat) >= 2: - # Legacy format: ["user msg", "assistant msg"] or ("user msg", "assistant msg") - user_msg, assistant_msg = chat[0], chat[1] - if user_msg: - messages.append({{"role": "user", "content": user_msg}}) - if assistant_msg: - messages.append({{"role": "assistant", "content": assistant_msg}}) - - # Add current message - messages.append({{"role": "user", "content": message}}) - - # Make API request with enhanced error handling - try: - print(f"🔄 Making API request to OpenRouter...") - print(f" Model: {{MODEL}}") - print(f" Messages: {{len(messages)}} in conversation") - - response = requests.post( - url="https://openrouter.ai/api/v1/chat/completions", - headers={{ - "Authorization": f"Bearer {{API_KEY}}", - "Content-Type": "application/json", - "HTTP-Referer": "https://huggingface.co", # Required by some providers - "X-Title": "HuggingFace Space" # Helpful for tracking - }}, - json={{ - "model": MODEL, - "messages": messages, - "temperature": {temperature}, - "max_tokens": {max_tokens} - }}, - timeout=30 - ) - - print(f"📡 API Response: {{response.status_code}}") - - if response.status_code == 200: - try: - result = response.json() - - # Enhanced validation of API response structure - if 'choices' not in result or not result['choices']: - print(f"⚠️ API response missing choices: {{result}}") - return "API Error: No response choices available" - elif 'message' not in result['choices'][0]: - print(f"⚠️ API response missing message: {{result}}") - return "API Error: No message in response" - elif 'content' not in result['choices'][0]['message']: - print(f"⚠️ API response missing content: {{result}}") - return "API Error: No content in message" - else: - content = result['choices'][0]['message']['content'] - - # Check for empty content - if not content or content.strip() == "": - print(f"⚠️ API returned empty content") - return "API Error: Empty response content" - - print(f"✅ API request successful") - return content - - except (KeyError, IndexError, json.JSONDecodeError) as e: - print(f"❌ Failed to parse API response: {{str(e)}}") - return f"API Error: Failed to parse response - {{str(e)}}" - elif response.status_code == 401: - error_msg = f"🔐 **Authentication Error**\\n\\n" - error_msg += f"Your API key appears to be invalid or expired.\\n\\n" - error_msg += f"**Troubleshooting:**\\n" - error_msg += f"1. Check that your **{api_key_var}** secret is set correctly\\n" - error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\\n" - error_msg += f"3. Ensure your key starts with `sk-or-`\\n" - error_msg += f"4. Check that you have credits on your OpenRouter account" - print(f"❌ API authentication failed: {{response.status_code}} - {{response.text[:200]}}") - return error_msg - elif response.status_code == 429: - error_msg = f"⏱️ **Rate Limit Exceeded**\\n\\n" - error_msg += f"Too many requests. Please wait a moment and try again.\\n\\n" - error_msg += f"**Troubleshooting:**\\n" - error_msg += f"1. Wait 30-60 seconds before trying again\\n" - error_msg += f"2. Check your OpenRouter usage limits\\n" - error_msg += f"3. Consider upgrading your OpenRouter plan" - print(f"❌ Rate limit exceeded: {{response.status_code}}") - return error_msg - elif response.status_code == 400: - try: - error_data = response.json() - error_message = error_data.get('error', {{}}).get('message', 'Unknown error') - except: - error_message = response.text - - error_msg = f"⚠️ **Request Error**\\n\\n" - error_msg += f"The API request was invalid:\\n" - error_msg += f"`{{error_message}}`\\n\\n" - if "model" in error_message.lower(): - error_msg += f"**Model Issue:** The model `{{MODEL}}` may not be available.\\n" - error_msg += f"Try switching to a different model in your Space configuration." - print(f"❌ Bad request: {{response.status_code}} - {{error_message}}") - return error_msg - else: - error_msg = f"🚫 **API Error {{response.status_code}}**\\n\\n" - error_msg += f"An unexpected error occurred. Please try again.\\n\\n" - error_msg += f"If this persists, check:\\n" - error_msg += f"1. OpenRouter service status\\n" - error_msg += f"2. Your API key and credits\\n" - error_msg += f"3. The model availability" - print(f"❌ API error: {{response.status_code}} - {{response.text[:200]}}") - return error_msg - - except requests.exceptions.Timeout: - error_msg = f"⏰ **Request Timeout**\\n\\n" - error_msg += f"The API request took too long (30s limit).\\n\\n" - error_msg += f"**Troubleshooting:**\\n" - error_msg += f"1. Try again with a shorter message\\n" - error_msg += f"2. Check your internet connection\\n" - error_msg += f"3. Try a different model" - print(f"❌ Request timeout after 30 seconds") - return error_msg - except requests.exceptions.ConnectionError: - error_msg = f"🌐 **Connection Error**\\n\\n" - error_msg += f"Could not connect to OpenRouter API.\\n\\n" - error_msg += f"**Troubleshooting:**\\n" - error_msg += f"1. Check your internet connection\\n" - error_msg += f"2. Check OpenRouter service status\\n" - error_msg += f"3. Try again in a few moments" - print(f"❌ Connection error to OpenRouter API") - return error_msg - except Exception as e: - error_msg = f"❌ **Unexpected Error**\\n\\n" - error_msg += f"An unexpected error occurred:\\n" - error_msg += f"`{{str(e)}}`\\n\\n" - error_msg += f"Please try again or contact support if this persists." - print(f"❌ Unexpected error: {{str(e)}}") - return error_msg - -# Access code verification -access_granted = gr.State(False) -_access_granted_global = False # Global fallback - -def verify_access_code(code): - \"\"\"Verify the access code\"\"\" - global _access_granted_global - if ACCESS_CODE is None: - _access_granted_global = True - return gr.update(visible=False), gr.update(visible=True), gr.update(value=True) - - if code == ACCESS_CODE: - _access_granted_global = True - return gr.update(visible=False), gr.update(visible=True), gr.update(value=True) - else: - _access_granted_global = False - return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False) - -def protected_generate_response(message, history): - \"\"\"Protected response function that checks access\"\"\" - # Check if access is granted via the global variable - if ACCESS_CODE is not None and not _access_granted_global: - return "Please enter the access code to continue." - return generate_response(message, history) - -# Global variable to store chat history for export -chat_history_store = [] - -def store_and_generate_response(message, history): - \"\"\"Wrapper function that stores history and generates response\"\"\" - global chat_history_store - - # Generate response using the protected function - response = protected_generate_response(message, history) - - # Convert current history to the format we need for export - # history comes in as [["user1", "bot1"], ["user2", "bot2"], ...] - chat_history_store = [] - if history: - for exchange in history: - if isinstance(exchange, (list, tuple)) and len(exchange) >= 2: - chat_history_store.append({{"role": "user", "content": exchange[0]}}) - chat_history_store.append({{"role": "assistant", "content": exchange[1]}}) - - # Add the current exchange - chat_history_store.append({{"role": "user", "content": message}}) - chat_history_store.append({{"role": "assistant", "content": response}}) - - return response - -def export_current_conversation(): - \"\"\"Export the current conversation\"\"\" - if not chat_history_store: - return gr.update(visible=False) - - markdown_content = export_conversation_to_markdown(chat_history_store) - - # Save to temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f: - f.write(markdown_content) - temp_file = f.name - - return gr.update(value=temp_file, visible=True) - -def export_conversation(history): - \"\"\"Export conversation to markdown file\"\"\" - if not history: - return gr.update(visible=False) - - markdown_content = export_conversation_to_markdown(history) - - # Save to temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f: - f.write(markdown_content) - temp_file = f.name - - return gr.update(value=temp_file, visible=True) - -# Configuration status display -def get_configuration_status(): - \"\"\"Generate a configuration status message for display\"\"\" - status_parts = [] - - # API Key status - status_parts.append("### 🔑 API Configuration") - if API_KEY_VALID: - status_parts.append("✅ **API Key:** Ready") - else: - status_parts.append("❌ **API Key:** Not configured") - status_parts.append(" Set `{api_key_var}` in Space secrets") - - # Model and parameters - status_parts.append("") # Blank line - status_parts.append("### 🤖 Model Settings") - status_parts.append(f"**Model:** {{MODEL.split('/')[-1]}}") - status_parts.append(f"**Temperature:** {temperature}") - status_parts.append(f"**Max Tokens:** {max_tokens}") - - # URL Context if configured - if GROUNDING_URLS: - status_parts.append("") # Blank line - status_parts.append("### 🔗 Context Sources") - status_parts.append(f"**URLs Configured:** {{len(GROUNDING_URLS)}}") - for i, url in enumerate(GROUNDING_URLS[:2], 1): - status_parts.append(f" {{i}}. {{url[:50]}}{{\'...\' if len(url) > 50 else \'\'}}") - if len(GROUNDING_URLS) > 2: - status_parts.append(f" ... and {{len(GROUNDING_URLS) - 2}} more") - - # Access control - if ACCESS_CODE is not None: - status_parts.append("") # Blank line - status_parts.append("### 🔐 Access Control") - status_parts.append("**Status:** Password protected") - - # System prompt - status_parts.append("") # Blank line - status_parts.append("### 📝 System Prompt") - # Show first 200 chars of system prompt - prompt_preview = SYSTEM_PROMPT[:200] + "..." if len(SYSTEM_PROMPT) > 200 else SYSTEM_PROMPT - status_parts.append(f"```\\n{{prompt_preview}}\\n```") - - return "\\n".join(status_parts) - -# Create interface with access code protection -# Dynamically set theme based on configuration -theme_class = getattr(gr.themes, THEME, gr.themes.Default) -with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo: - gr.Markdown(f"# {{SPACE_NAME}}") - gr.Markdown(SPACE_DESCRIPTION) - - # Access code section (shown only if ACCESS_CODE is set) - with gr.Column(visible=(ACCESS_CODE is not None)) as access_section: - gr.Markdown("### 🔐 Access Required") - gr.Markdown("Please enter the access code provided by your instructor:") - - access_input = gr.Textbox( - label="Access Code", - placeholder="Enter access code...", - type="password" - ) - access_btn = gr.Button("Submit", variant="primary") - access_error = gr.Markdown(visible=False) - - # Main chat interface (hidden until access granted) - with gr.Column(visible=(ACCESS_CODE is None)) as chat_section: - chat_interface = gr.ChatInterface( - fn=store_and_generate_response, # Use wrapper function to store history - title="", # Title already shown above - description="", # Description already shown above - examples={examples}, - type="messages" # Use modern message format for better compatibility - ) - - # Export functionality - with gr.Row(): - export_btn = gr.Button("📥 Export Conversation", variant="secondary", size="sm") - export_file = gr.File(label="Download", visible=False) - - # Connect export functionality - export_btn.click( - export_current_conversation, - outputs=[export_file] - ) - - # Configuration status - with gr.Accordion("📊 Configuration Status", open=True): - gr.Markdown(get_configuration_status()) - - # Connect access verification - if ACCESS_CODE is not None: - access_btn.click( - verify_access_code, - inputs=[access_input], - outputs=[access_error, chat_section, access_granted] - ) - access_input.submit( - verify_access_code, - inputs=[access_input], - outputs=[access_error, chat_section, access_granted] - ) - - # Faculty Configuration Section - appears at the bottom with password protection - with gr.Accordion("🔧 Faculty Configuration", open=False, visible=True) as faculty_section: - gr.Markdown("**Faculty Only:** Edit assistant configuration. Requires CONFIG_CODE secret.") - - # Check if faculty password is configured - FACULTY_PASSWORD = os.environ.get("CONFIG_CODE", "").strip() - - if FACULTY_PASSWORD: - faculty_auth_state = gr.State(False) - - # Authentication row - with gr.Column() as faculty_auth_row: - with gr.Row(): - faculty_password_input = gr.Textbox( - label="Faculty Password", - type="password", - placeholder="Enter faculty configuration password", - scale=3 - ) - faculty_auth_btn = gr.Button("Unlock Configuration", variant="primary", scale=1) - faculty_auth_status = gr.Markdown("") - - # Configuration editor (hidden until authenticated) - with gr.Column(visible=False) as faculty_config_section: - gr.Markdown("### Edit Assistant Configuration") - gr.Markdown("⚠️ **Warning:** Changes will affect all users immediately.") - - # Load current configuration - try: - with open('config.json', 'r') as f: - current_config = json.load(f) - except: - current_config = {{ - 'system_prompt': SYSTEM_PROMPT, - 'temperature': {temperature}, - 'max_tokens': {max_tokens}, - 'locked': False - }} - - # Editable fields - edit_system_prompt = gr.Textbox( - label="System Prompt", - value=current_config.get('system_prompt', SYSTEM_PROMPT), - lines=5 - ) - - with gr.Row(): - edit_temperature = gr.Slider( - label="Temperature", - minimum=0, - maximum=2, - value=current_config.get('temperature', {temperature}), - step=0.1 - ) - edit_max_tokens = gr.Slider( - label="Max Tokens", - minimum=50, - maximum=4096, - value=current_config.get('max_tokens', {max_tokens}), - step=50 - ) - - config_locked = gr.Checkbox( - label="Lock Configuration (Prevent further edits)", - value=current_config.get('locked', False) - ) - - with gr.Row(): - save_config_btn = gr.Button("💾 Save Configuration", variant="primary") - reset_config_btn = gr.Button("↩️ Reset to Defaults", variant="secondary") - - config_status = gr.Markdown("") - - # Faculty authentication function - def verify_faculty_password(password): - if password == FACULTY_PASSWORD: - return ( - gr.update(value="✅ Authentication successful!"), - gr.update(visible=False), # Hide auth row - gr.update(visible=True), # Show config section - True # Update auth state - ) - else: - return ( - gr.update(value="❌ Invalid password"), - gr.update(visible=True), # Keep auth row visible - gr.update(visible=False), # Keep config hidden - False # Auth failed - ) - - # Save configuration function - def save_configuration(new_prompt, new_temp, new_tokens, lock_config, is_authenticated): - if not is_authenticated: - return "❌ Not authenticated" - - # Check if configuration is already locked - try: - with open('config.json', 'r') as f: - existing_config = json.load(f) - if existing_config.get('locked', False): - return "🔒 Configuration is locked and cannot be modified" - except: - pass - - # Save new configuration - new_config = {{ - 'system_prompt': new_prompt, - 'temperature': new_temp, - 'max_tokens': int(new_tokens), - 'locked': lock_config, - 'last_modified': datetime.now().isoformat(), - 'modified_by': 'faculty' - }} - - try: - with open('config.json', 'w') as f: - json.dump(new_config, f, indent=2) - - # Update global variables - global SYSTEM_PROMPT, temperature, max_tokens - SYSTEM_PROMPT = new_prompt - temperature = new_temp - max_tokens = int(new_tokens) - - return f"✅ Configuration saved successfully at {{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}}" - except Exception as e: - return f"❌ Error saving configuration: {{str(e)}}" - - # Reset configuration function - def reset_configuration(is_authenticated): - if not is_authenticated: - return "❌ Not authenticated", gr.update(), gr.update(), gr.update() - - # Check if locked - try: - with open('config.json', 'r') as f: - existing_config = json.load(f) - if existing_config.get('locked', False): - return "🔒 Configuration is locked", gr.update(), gr.update(), gr.update() - except: - pass - - # Reset to original values - return ( - "↩️ Reset to default values", - gr.update(value=SYSTEM_PROMPT), - gr.update(value={temperature}), - gr.update(value={max_tokens}) - ) - - # Connect authentication - faculty_auth_btn.click( - verify_faculty_password, - inputs=[faculty_password_input], - outputs=[faculty_auth_status, faculty_auth_row, faculty_config_section, faculty_auth_state] - ) - - faculty_password_input.submit( - verify_faculty_password, - inputs=[faculty_password_input], - outputs=[faculty_auth_status, faculty_auth_row, faculty_config_section, faculty_auth_state] - ) - - # Connect configuration buttons - save_config_btn.click( - save_configuration, - inputs=[edit_system_prompt, edit_temperature, edit_max_tokens, config_locked, faculty_auth_state], - outputs=[config_status] - ) - - reset_config_btn.click( - reset_configuration, - inputs=[faculty_auth_state], - outputs=[config_status, edit_system_prompt, edit_temperature, edit_max_tokens] - ) - else: - gr.Markdown("ℹ️ Faculty configuration is not enabled. Set CONFIG_CODE in Space secrets to enable.") - -if __name__ == "__main__": - demo.launch() -''' - -# Available models - Updated with valid OpenRouter model IDs -MODELS = [ - "google/gemini-2.0-flash-001", # Fast, reliable, general tasks - "google/gemma-3-27b-it", # High-performance open model - "anthropic/claude-3.5-sonnet", # Superior LaTeX rendering and mathematical reasoning - "anthropic/claude-3.5-haiku", # Complex reasoning and analysis - "openai/gpt-4o-mini-search-preview", # Balanced performance and cost with search - "openai/gpt-4.1-nano", # Lightweight OpenAI model - "nvidia/llama-3.1-nemotron-70b-instruct", # Large open-source model - "mistralai/devstral-small" # Coding-focused model -] - - -def get_grounding_context(urls): - """Fetch context from grounding URLs""" - if not urls: - return "" - - context_parts = [] - for i, url in enumerate(urls, 1): - if url and url.strip(): - content = enhanced_fetch_url_content(url.strip()) - # Add priority indicators - priority_label = "PRIMARY" if i <= 2 else "SECONDARY" - context_parts.append(f"[{priority_label}] Context from URL {i} ({url}):\n{content}") - - if context_parts: - return "\n\n" + "\n\n".join(context_parts) + "\n\n" - return "" - -# Removed create_readme function - no longer generating README.md - pass # Function removed - README.md no longer generated - -def create_requirements(): - """Generate requirements.txt with latest versions""" - return "gradio>=5.38.0\nrequests>=2.32.3\nbeautifulsoup4>=4.12.3\npython-dotenv>=1.0.0" - -def generate_zip(title, description, system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code_field="", theme="default", url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""): - """Generate deployable zip file""" - - # Process examples - if examples_text and examples_text.strip(): - examples_list = [ex.strip() for ex in examples_text.split('\n') if ex.strip()] - examples_python = repr(examples_list) # Convert to Python literal representation - else: - examples_python = repr([ - "Hello! How can you help me?", - "Tell me something interesting", - "What can you do?" - ]) - - # Process grounding URLs - grounding_urls = [] - for url in [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]: - if url and url.strip(): - grounding_urls.append(url.strip()) - - # Use the provided system prompt directly - - # Create config - config = { - 'name': title or 'AI Assistant', # Use provided title or default - 'description': description or 'A customizable AI assistant', # Use provided description or default - 'system_prompt': system_prompt, - 'model': model, - 'api_key_var': api_key_var, - 'temperature': temperature, - 'max_tokens': int(max_tokens), - 'examples': examples_python, - 'grounding_urls': json.dumps(grounding_urls), - 'enable_dynamic_urls': True, # Always enabled - 'theme': theme.capitalize() if theme != "default" else "Default" # Capitalize theme name for gr.themes - } - - # Generate files - app_content = SPACE_TEMPLATE.format(**config) - requirements_content = create_requirements() - - # Create zip file with clean naming - filename = "ai_assistant_space.zip" - - # Create zip in memory and save to disk - zip_buffer = io.BytesIO() - with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: - zip_file.writestr('app.py', app_content) - zip_file.writestr('requirements.txt', requirements_content) - zip_file.writestr('config.json', json.dumps(config, indent=2)) - - # Write zip to file - zip_buffer.seek(0) - with open(filename, 'wb') as f: - f.write(zip_buffer.getvalue()) - - return filename - -# Define callback functions outside the interface - -def update_sandbox_preview(config_data): - """Update the sandbox preview with generated content""" - if not config_data: - return "Generate a space configuration to see preview here.", "
No preview available
" - - # Create preview info - preview_text = f"""**Space Configuration:** -- **Name:** {config_data.get('name', 'N/A')} -- **Model:** {config_data.get('model', 'N/A')} -- **Temperature:** {config_data.get('temperature', 'N/A')} -- **Max Tokens:** {config_data.get('max_tokens', 'N/A')} -- **Dynamic URLs:** {'✅ Enabled' if config_data.get('enable_dynamic_urls') else '❌ Disabled'} - -**System Prompt Preview:** -> {config_data.get('system_prompt', 'No system prompt configured')[:500]}{'...' if len(config_data.get('system_prompt', '')) > 500 else ''} - -**Deployment Package:** `{config_data.get('filename', 'Not generated')}`""" - - # Create a basic HTML preview of the chat interface - preview_html = f""" -
-

{config_data.get('name', 'Chat Interface')}

-

{config_data.get('description', 'A customizable AI chat interface')}

- -
-
Chat Interface Preview
-
- Assistant: Hello! I'm ready to help you. How can I assist you today? -
-
- -
- - -
- -
- Configuration: Model: {config_data.get('model', 'N/A')} | Temperature: {config_data.get('temperature', 'N/A')} | Max Tokens: {config_data.get('max_tokens', 'N/A')} -
-
- """ - - return preview_text, preview_html - -def on_preview_combined(title, description, system_prompt, model, theme, temperature, max_tokens, examples_text, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""): - """Generate configuration and return preview updates""" - # Removed name validation since title field no longer exists - - try: - # Use the system prompt directly (template selector already updates it) - if not system_prompt or not system_prompt.strip(): - return ( - {}, - gr.update(value="**Error:** Please provide a System Prompt for the assistant", visible=True), - gr.update(visible=False), - gr.update(value="Configuration will appear here after preview generation."), - *[gr.update() for _ in range(10)], # 10 URL updates - gr.update(), # preview_add_url_btn - gr.update(), # preview_remove_url_btn - 2, # preview_url_count - *[gr.update(visible=False) for _ in range(3)] # 3 example button updates - ) - - final_system_prompt = system_prompt.strip() - - # Process examples like the deployment package - if examples_text and examples_text.strip(): - examples_list = [ex.strip() for ex in examples_text.split('\n') if ex.strip()] - else: - examples_list = [ - "Hello! How can you help me?", - "Tell me something interesting", - "What can you do?" - ] - - # Create configuration for preview - config_data = { - 'name': title or 'AI Assistant', - 'description': description or 'A customizable AI assistant', - 'system_prompt': final_system_prompt, - 'model': model, - 'theme': theme, - 'temperature': temperature, - 'max_tokens': max_tokens, - 'enable_dynamic_urls': True, # Always enabled - 'url1': url1, - 'url2': url2, - 'url3': url3, - 'url4': url4, - 'url5': url5, - 'url6': url6, - 'url7': url7, - 'url8': url8, - 'url9': url9, - 'url10': url10, - 'examples_text': examples_text, - 'examples_list': examples_list, # Processed examples for preview - 'preview_ready': True - } - - # Generate preview displays with example prompts - examples_preview = "\n".join([f"• {ex}" for ex in examples_list[:3]]) # Show first 3 examples - - preview_text = f"""**{title or 'AI Assistant'}** is ready to test. Use the example prompts below or type your own message.""" - config_display = f"""> **Configuration**: -- **Name:** {title or 'AI Assistant'} -- **Model:** {model} -- **Theme:** {theme} -- **Temperature:** {temperature} -- **Max Response Tokens:** {max_tokens} - -**System Prompt:** -{final_system_prompt} - -**Example Prompts:** -{examples_text if examples_text and examples_text.strip() else 'No example prompts configured'} -""" - - # Show success notification - gr.Info(f"✅ Preview generated successfully! Switch to Preview tab.") - - # Determine how many URLs are configured - all_urls = [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10] - url_count = 2 # Start with 2 (always visible) - for i, url in enumerate(all_urls[2:], start=3): # Check urls 3-10 - if url and url.strip(): - url_count = i - else: - break - - # Create URL updates for all preview URLs - url_updates = [] - for i in range(1, 11): # URLs 1-10 - url_value = all_urls[i-1] if i <= len(all_urls) else "" - if i <= 2: # URLs 1-2 are always visible - url_updates.append(gr.update(value=url_value)) - else: # URLs 3-10 - url_updates.append(gr.update(value=url_value, visible=(i <= url_count))) - - # Update button states - secondary_count = url_count - 2 # Number of secondary URLs - if url_count >= 10: - preview_add_btn_update = gr.update(value="Max Secondary URLs (8/8)", interactive=False) - else: - preview_add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)", interactive=True) - - preview_remove_btn_update = gr.update(visible=(url_count > 2)) - - # Update example buttons - example_btn_updates = [] - for i in range(3): - if i < len(examples_list): - # Add click icon and truncate text nicely - btn_text = f"💬 {examples_list[i][:45]}{'...' if len(examples_list[i]) > 45 else ''}" - example_btn_updates.append(gr.update(value=btn_text, visible=True)) - else: - example_btn_updates.append(gr.update(visible=False)) - - return ( - config_data, - gr.update(value=preview_text, visible=True), - gr.update(visible=True), - gr.update(value=config_display), - *url_updates, # Unpack all 10 URL updates - preview_add_btn_update, - preview_remove_btn_update, - url_count, - *example_btn_updates # Add example button updates - ) - - except Exception as e: - return ( - {}, - gr.update(value=f"**Error:** {str(e)}", visible=True), - gr.update(visible=False), - gr.update(value="Configuration will appear here after preview generation."), - *[gr.update() for _ in range(10)], # 10 URL updates - gr.update(), # preview_add_url_btn - gr.update(), # preview_remove_url_btn - 2, # preview_url_count - *[gr.update(visible=False) for _ in range(3)] # 3 example button updates - ) - -def update_preview_display(config_data): - """Update preview display based on config data""" - if not config_data or not config_data.get('preview_ready'): - return ( - gr.update(value="**Status:** Configure your space in the Configuration tab and click 'Preview Deployment Package' to see your assistant here.", visible=True), - gr.update(visible=False), - gr.update(value="Configuration will appear here after preview generation.") - ) - - # Generate example prompts display - examples_list = config_data.get('examples_list', []) - examples_preview = "\n".join([f"• {ex}" for ex in examples_list[:3]]) # Show first 3 examples - - preview_text = f"""**AI Assistant** is ready to test. Use the example prompts below or type your own message.""" - - config_display = f"""### Current Configuration - -**Model Settings:** -- **Model:** {config_data['model']} -- **Theme:** {config_data.get('theme', 'default')} -- **Temperature:** {config_data['temperature']} -- **Max Response Tokens:** {config_data['max_tokens']} - -**Features:** -- **Dynamic URL Fetching:** {'✅ Enabled' if config_data['enable_dynamic_urls'] else '❌ Disabled'} - -**System Prompt:** -{config_data['system_prompt']} - -**Example Prompts:** -{config_data.get('examples_text', 'No example prompts configured') if config_data.get('examples_text', '').strip() else 'No example prompts configured'} -""" - - return ( - gr.update(value=preview_text, visible=True), - gr.update(visible=True), - gr.update(value=config_display) - ) - -def preview_chat_response(message, history, config_data, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""): - """Generate response for preview chat using actual OpenRouter API""" - if not config_data or not message: - return "", history - - # Get API key from environment - api_key = os.environ.get("OPENROUTER_API_KEY") - - if not api_key: - response = f"""🔑 **API Key Required for Preview** - -To test your assistant with real API responses, please: - -1. Get your OpenRouter API key from: https://openrouter.ai/keys -2. Set it as an environment variable: `export OPENROUTER_API_KEY=your_key_here` -3. Or add it to your `.env` file: `OPENROUTER_API_KEY=your_key_here` - -**Your Configuration:** -- **Model:** {config_data.get('model', 'unknown model')} -- **Temperature:** {config_data.get('temperature', 0.7)} -- **Max Tokens:** {config_data.get('max_tokens', 500)} - -**System Prompt Preview:** -{config_data.get('system_prompt', '')[:200]}{'...' if len(config_data.get('system_prompt', '')) > 200 else ''} - -Once you set your API key, you'll be able to test real conversations in this preview.""" - history.append({"role": "user", "content": message}) - history.append({"role": "assistant", "content": response}) - return "", history - - try: - # Get grounding context from URLs - prioritize config_data URLs, fallback to preview tab URLs - config_urls = [ - config_data.get('url1', ''), - config_data.get('url2', ''), - config_data.get('url3', ''), - config_data.get('url4', ''), - config_data.get('url5', ''), - config_data.get('url6', ''), - config_data.get('url7', ''), - config_data.get('url8', ''), - config_data.get('url9', ''), - config_data.get('url10', '') - ] - # Use config URLs if available, otherwise use preview tab URLs - grounding_urls = config_urls if any(url for url in config_urls if url) else [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10] - grounding_context = get_cached_grounding_context([url for url in grounding_urls if url and url.strip()]) - - - # If dynamic URLs are enabled, check message for URLs to fetch - dynamic_context = "" - if config_data.get('enable_dynamic_urls'): - urls_in_message = extract_urls_from_text(message) - if urls_in_message: - dynamic_context_parts = [] - for url in urls_in_message[:3]: # Increased limit to 3 URLs with enhanced processing - content = enhanced_fetch_url_content(url) - dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}") - if dynamic_context_parts: - dynamic_context = "\n".join(dynamic_context_parts) - - # Build enhanced system prompt with all contexts - enhanced_system_prompt = config_data.get('system_prompt', '') + grounding_context + dynamic_context - - # Build messages array for the API - messages = [{"role": "system", "content": enhanced_system_prompt}] - - # Add conversation history - handle both formats for backwards compatibility - for chat in history: - if isinstance(chat, dict): - # New format: {"role": "user", "content": "..."} - messages.append(chat) - elif isinstance(chat, list) and len(chat) >= 2: - # Legacy format: [user_msg, assistant_msg] - user_msg, assistant_msg = chat[0], chat[1] - if user_msg: - messages.append({"role": "user", "content": user_msg}) - if assistant_msg: - messages.append({"role": "assistant", "content": assistant_msg}) - - # Add current message - messages.append({"role": "user", "content": message}) - - # Debug: Log the request being sent - request_payload = { - "model": config_data.get('model', 'google/gemini-2.0-flash-001'), - "messages": messages, - "temperature": config_data.get('temperature', 0.7), - "max_tokens": config_data.get('max_tokens', 500), - "tools": None # Explicitly disable tool/function calling - } - - # Make API request to OpenRouter - response = requests.post( - url="https://openrouter.ai/api/v1/chat/completions", - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - }, - json=request_payload, - timeout=30 - ) - - if response.status_code == 200: - try: - response_data = response.json() - - # Check if response has expected structure - if 'choices' not in response_data or not response_data['choices']: - assistant_response = f"[Preview Debug] No choices in API response. Response: {response_data}" - elif 'message' not in response_data['choices'][0]: - assistant_response = f"[Preview Debug] No message in first choice. Response: {response_data}" - elif 'content' not in response_data['choices'][0]['message']: - assistant_response = f"[Preview Debug] No content in message. Response: {response_data}" - else: - assistant_content = response_data['choices'][0]['message']['content'] - - # Debug: Check if content is empty - if not assistant_content or assistant_content.strip() == "": - assistant_response = f"[Preview Debug] Empty content from API. Messages sent: {len(messages)} messages, last user message: '{message}', model: {request_payload['model']}" - else: - # Use the content directly - no preview indicator needed - assistant_response = assistant_content - - except (KeyError, IndexError, json.JSONDecodeError) as e: - assistant_response = f"[Preview Error] Failed to parse API response: {str(e)}. Raw response: {response.text[:500]}" - else: - assistant_response = f"[Preview Error] API Error: {response.status_code} - {response.text[:500]}" - - except Exception as e: - assistant_response = f"[Preview Error] {str(e)}" - - # Return in the new messages format for Gradio 5.x - history.append({"role": "user", "content": message}) - history.append({"role": "assistant", "content": assistant_response}) - return "", history - -def clear_preview_chat(): - """Clear preview chat""" - return "", [] - -def set_example_prompt(example_text): - """Set example prompt in the text input""" - return example_text - -def export_preview_conversation(history, config_data=None): - """Export preview conversation to markdown""" - if not history: - return gr.update(visible=False) - - markdown_content = export_conversation_to_markdown(history, config_data) - - # Save to temporary file - import tempfile - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: - f.write(markdown_content) - temp_file = f.name - - return gr.update(value=temp_file, visible=True) - -def on_generate(title, description, system_prompt, model, theme, api_key_var, temperature, max_tokens, examples_text, access_code, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10): - try: - # Validate required fields - if not system_prompt or not system_prompt.strip(): - return gr.update(value="Error: Please provide a System Prompt for the assistant", visible=True), gr.update(visible=False), {} - - if not title or not title.strip(): - return gr.update(value="Error: Please provide an Assistant Name", visible=True), gr.update(visible=False), {} - - final_system_prompt = system_prompt.strip() - - filename = generate_zip(title, description, final_system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code, theme, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10) - - success_msg = f"""**Deployment package ready!** - -**File**: `{filename}` - -**What's included:** -- `app.py` - Ready-to-deploy chat interface (Gradio 5.38.0) -- `requirements.txt` - Latest dependencies -- `config.json` - Configuration backup - -**Next steps:** -1. Download the zip file below -2. Go to https://huggingface.co/spaces and create a new Space -3. Upload ALL files from the zip to your Space -4. Set your `{api_key_var}` secret in Space settings - -**Your Space will be live in minutes!**""" - - # Update sandbox preview - config_data = { - 'name': title, - 'description': description, - 'system_prompt': final_system_prompt, - 'model': model, - 'temperature': temperature, - 'max_tokens': max_tokens, - 'enable_dynamic_urls': True, # Always enabled - 'filename': filename - } - - return gr.update(value=success_msg, visible=True), gr.update(value=filename, visible=True), config_data - - except Exception as e: - return gr.update(value=f"Error: {str(e)}", visible=True), gr.update(visible=False), {} - -# Global cache for URL content to avoid re-crawling -url_content_cache = {} - -def get_cached_grounding_context(urls): - """Get grounding context with caching to avoid re-crawling same URLs""" - if not urls: - return "" - - # Filter valid URLs - valid_urls = [url for url in urls if url and url.strip()] - if not valid_urls: - return "" - - # Create cache key from sorted URLs - cache_key = tuple(sorted(valid_urls)) - - # Check if we already have this content cached - if cache_key in url_content_cache: - return url_content_cache[cache_key] - - # If not cached, fetch using simple HTTP requests - grounding_context = get_grounding_context_simple(valid_urls) - - # Cache the result - url_content_cache[cache_key] = grounding_context - - return grounding_context - - -def respond(message, chat_history, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""): - # Make actual API request to OpenRouter - import os - import requests - - # Get API key from environment - api_key = os.environ.get("OPENROUTER_API_KEY") - - if not api_key: - response = "Please set your OPENROUTER_API_KEY in the Space settings to use the chat support." - chat_history.append({"role": "user", "content": message}) - chat_history.append({"role": "assistant", "content": response}) - return "", chat_history - - # Get grounding context from URLs using cached approach - grounding_urls = [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10] - grounding_context = get_cached_grounding_context(grounding_urls) - - # Build enhanced system prompt with grounding context - base_system_prompt = """You are an expert assistant specializing in Gradio configurations for HuggingFace Spaces. You have deep knowledge of: -- Gradio interface components and layouts -- HuggingFace Spaces configuration (YAML frontmatter, secrets, environment variables) -- Deployment best practices for Gradio apps on HuggingFace -- Space settings, SDK versions, and hardware requirements -- Troubleshooting common Gradio and HuggingFace Spaces issues -- Integration with various APIs and models through Gradio interfaces - -Provide specific, technical guidance focused on Gradio implementation details and HuggingFace Spaces deployment. Include code examples when relevant. Keep responses concise and actionable.""" - - enhanced_system_prompt = base_system_prompt + grounding_context - - # Build conversation history for API - messages = [{ - "role": "system", - "content": enhanced_system_prompt - }] - - # Add conversation history - Support both new messages format and legacy tuple format - for chat in chat_history: - if isinstance(chat, dict): - # New format: {"role": "user", "content": "..."} - messages.append(chat) - elif isinstance(chat, (list, tuple)) and len(chat) >= 2: - # Legacy format: ("user msg", "bot msg") - user_msg, assistant_msg = chat[0], chat[1] - if user_msg: - messages.append({"role": "user", "content": user_msg}) - if assistant_msg: - messages.append({"role": "assistant", "content": assistant_msg}) - - # Add current message - messages.append({"role": "user", "content": message}) - - try: - # Make API request to OpenRouter - response = requests.post( - url="https://openrouter.ai/api/v1/chat/completions", - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - }, - json={ - "model": "google/gemini-2.0-flash-001", - "messages": messages, - "temperature": 0.7, - "max_tokens": 500 - } - ) - - if response.status_code == 200: - assistant_response = response.json()['choices'][0]['message']['content'] - else: - assistant_response = f"Error: {response.status_code} - {response.text}" - - except Exception as e: - assistant_response = f"Error: {str(e)}" - - chat_history.append({"role": "user", "content": message}) - chat_history.append({"role": "assistant", "content": assistant_response}) - return "", chat_history - -def clear_chat(): - return "", [] - - - -def add_urls(count): - """Show additional URL fields""" - new_count = min(count + 1, 10) - - # Create visibility updates for all URL fields (3-10) - url_updates = [] - for i in range(3, 11): # URLs 3-10 - if i <= new_count: - url_updates.append(gr.update(visible=True)) - else: - url_updates.append(gr.update(visible=False)) - - # Update button states - secondary_count = new_count - 2 # Number of secondary URLs - if new_count >= 10: - add_btn_update = gr.update(value="Max Secondary URLs (8/8)", interactive=False) - else: - add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)") - - remove_btn_update = gr.update(visible=True) - - return (*url_updates, add_btn_update, remove_btn_update, new_count) - -def remove_urls(count): - """Hide URL fields""" - new_count = max(count - 1, 4) - - # Create visibility updates for all URL fields (3-10) - url_updates = [] - for i in range(3, 11): # URLs 3-10 - if i <= new_count: - url_updates.append(gr.update(visible=True)) - else: - url_updates.append(gr.update(visible=False, value="")) - - # Update button states - secondary_count = new_count - 2 # Number of secondary URLs - add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)", interactive=True) - - if new_count <= 4: - remove_btn_update = gr.update(visible=False) - else: - remove_btn_update = gr.update(visible=True) - - return (*url_updates, add_btn_update, remove_btn_update, new_count) - +""" +HuggingFace Space Generator - Refactored with Gradio 5.x Best Practices +Creates customizable AI chat interfaces for deployment on HuggingFace Spaces +""" +import gradio as gr +import json +import zipfile +import io +import os +from datetime import datetime +from dotenv import load_dotenv +from pathlib import Path +# Import our shared utilities +from utils import ( + get_theme, fetch_url_content, create_safe_filename, + export_conversation_to_markdown, process_file_upload, + ConfigurationManager, get_model_choices, AVAILABLE_THEMES, + extract_urls_from_text +) -def toggle_template(template_choice, current_prompt, cached_custom_prompt): - """Toggle between different assistant templates""" - # If we're switching away from "None", cache the current custom prompt - if template_choice != "None" and current_prompt and current_prompt.strip(): - # Check if the current prompt is not a template prompt - research_prompt = "You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation." - socratic_prompt = "You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinking—analysis, synthesis, or evaluation—rather than recall." - mathematics_prompt = r"You are an AI assistant specialized in mathematics and statistics who guides users through problem-solving rather than providing direct answers. You help users discover solutions by asking strategic questions ('What do we know so far?' 'What method might apply here?' 'Can you identify a pattern?'), prompting them to explain their reasoning, and offering hints that build on their current understanding. Format all mathematical expressions in LaTeX (inline: $x^2 + y^2 = r^2$, display: $$\int_a^b f(x)dx$$). When users are stuck, provide scaffolded support: suggest examining simpler cases, identifying relevant formulas or theorems, or breaking the problem into smaller parts. Use multiple representations to illuminate different aspects of the problem, validate partial progress to build confidence, and help users recognize and correct their own errors through targeted questions rather than corrections. Your goal is to develop problem-solving skills and mathematical reasoning, not just arrive at answers." - - # Only cache if it's not one of the template prompts - if current_prompt != research_prompt and current_prompt != socratic_prompt and current_prompt != mathematics_prompt: - cached_custom_prompt = current_prompt - - if template_choice == "Research Template": - research_prompt = "You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation." - return ( - gr.update(value=research_prompt), # Update main system prompt - cached_custom_prompt # Return the cached prompt - ) - elif template_choice == "Socratic Template": - socratic_prompt = "You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinking—analysis, synthesis, or evaluation—rather than recall." - return ( - gr.update(value=socratic_prompt), # Update main system prompt - cached_custom_prompt # Return the cached prompt - ) - elif template_choice == "Mathematics Template": - mathematics_prompt = r"You are an AI assistant specialized in mathematics and statistics who guides users through problem-solving rather than providing direct answers. You help users discover solutions by asking strategic questions ('What do we know so far?' 'What method might apply here?' 'Can you identify a pattern?'), prompting them to explain their reasoning, and offering hints that build on their current understanding. Format all mathematical expressions in LaTeX (inline: $x^2 + y^2 = r^2$, display: $$\int_a^b f(x)dx$$). When users are stuck, provide scaffolded support: suggest examining simpler cases, identifying relevant formulas or theorems, or breaking the problem into smaller parts. Use multiple representations to illuminate different aspects of the problem, validate partial progress to build confidence, and help users recognize and correct their own errors through targeted questions rather than corrections. Your goal is to develop problem-solving skills and mathematical reasoning, not just arrive at answers." - return ( - gr.update(value=mathematics_prompt), # Update main system prompt - cached_custom_prompt # Return the cached prompt - ) - else: # "None" or any other value - # Restore the cached custom prompt if we have one - prompt_value = cached_custom_prompt if cached_custom_prompt else "" - return ( - gr.update(value=prompt_value), # Restore cached prompt or clear - cached_custom_prompt # Return the cached prompt - ) +# Load environment variables +load_dotenv() +# Load templates +try: + from space_template import get_template, validate_template + print("Loaded space template") +except Exception as e: + print(f"Could not load space_template.py: {e}") + # Fallback template will be defined if needed -# Create Gradio interface with proper tab structure and fixed configuration -with gr.Blocks( - title="Chat U/I Helper", - css=""" - /* Custom CSS to fix styling issues */ - .gradio-container { - max-width: 1200px !important; - margin: 0 auto; - } - - /* Fix tab styling */ - .tab-nav { - border-bottom: 1px solid #e0e0e0; - } - - /* Fix button styling */ - .btn { - border-radius: 6px; - } - - /* Fix chat interface styling */ - .chat-interface { - border-radius: 8px; - border: 1px solid #e0e0e0; - } - - /* Hide gradio footer to avoid manifest issues */ - .gradio-footer { - display: none !important; +# Load academic templates if available +try: + with open('academic_templates.json', 'r') as f: + ACADEMIC_TEMPLATES = json.load(f) + print(f"Loaded {len(ACADEMIC_TEMPLATES)} academic templates") +except Exception as e: + print(f"Could not load academic templates: {e}") + ACADEMIC_TEMPLATES = {} + + +class SpaceGenerator: + """Main application class for generating HuggingFace Spaces""" + + def __init__(self): + self.default_config = { + 'name': 'AI Assistant', + 'tagline': 'A customizable AI assistant', + 'description': 'A versatile AI assistant powered by advanced language models. Configure it to meet your specific needs with custom prompts, examples, and grounding URLs.', + 'system_prompt': 'You are a helpful AI assistant.', + 'model': 'google/gemini-2.0-flash-001', + 'api_key_var': 'API_KEY', + 'temperature': 0.7, + 'max_tokens': 750, + 'theme': 'Default', + 'grounding_urls': [], + 'enable_dynamic_urls': True, + 'enable_file_upload': True, + 'examples': [ + "Hello! How can you help me?", + "Tell me something interesting", + "What can you do?" + ] } - /* Fix accordion styling */ - .accordion { - border: 1px solid #e0e0e0; - border-radius: 6px; - } - """, - theme="default", - head=""" - - """, - js=""" - function() { - // Prevent manifest.json requests and other common errors - if (typeof window !== 'undefined') { - // Override fetch to handle manifest.json requests - const originalFetch = window.fetch; - window.fetch = function(url, options) { - // Handle both string URLs and URL objects - const urlString = typeof url === 'string' ? url : url.toString(); - - if (urlString.includes('manifest.json')) { - return Promise.resolve(new Response('{}', { - status: 200, - headers: { 'Content-Type': 'application/json' } - })); - } - - // Handle favicon requests - if (urlString.includes('favicon.ico')) { - return Promise.resolve(new Response('', { status: 204 })); - } - - return originalFetch.apply(this, arguments); - }; + self.config_manager = ConfigurationManager(self.default_config) + self.current_config = {} + self.url_content_cache = {} + # Cache for custom values when switching templates + self.custom_values_cache = {} + + def create_interface(self) -> gr.Blocks: + """Create the main Gradio interface""" + theme = get_theme("Default") # Using Default theme for the generator + + with gr.Blocks(title="ChatUI Helper", theme=theme) as demo: + # Header + gr.Markdown("# ChatUI Helper") + gr.Markdown("Create customizable AI chat interfaces for deployment on HuggingFace Spaces") + + # Shared state - create these first so they can be referenced in tabs + self.config_state = gr.State({}) + self.preview_chat_history = gr.State([]) + self.previous_template = gr.State("None (Custom)") + self.template_cache = gr.State({}) + + # Main tabs + with gr.Tabs() as main_tabs: + self.main_tabs = main_tabs # Store reference for tab switching - // Prevent postMessage origin errors - window.addEventListener('message', function(event) { - try { - if (event.origin && event.origin !== window.location.origin) { - event.stopImmediatePropagation(); - return false; - } - } catch (e) { - // Silently ignore origin check errors - } - }, true); + # Configuration Tab + with gr.Tab("📋 Configuration"): + self._create_configuration_tab() - // Prevent console errors from missing resources - window.addEventListener('error', function(e) { - if (e.target && e.target.src) { - const src = e.target.src; - if (src.includes('manifest.json') || src.includes('favicon.ico')) { - e.preventDefault(); - return false; - } - } - }, true); + # Preview Tab + with gr.Tab("👁️ Preview"): + self._create_preview_tab() - // Override console.error to filter out known harmless errors - const originalConsoleError = console.error; - console.error = function(...args) { - const message = args.join(' '); - if (message.includes('manifest.json') || - message.includes('favicon.ico') || - message.includes('postMessage') || - message.includes('target origin')) { - return; // Suppress these specific errors - } - originalConsoleError.apply(console, arguments); - }; - } - } - """ -) as demo: - # Global state for cross-tab functionality - sandbox_state = gr.State({}) - preview_config_state = gr.State({}) - - # Global status components that will be defined later - preview_status = None - preview_chat_section = None - config_display = None - - with gr.Tabs(): - with gr.Tab("Configuration"): - gr.Markdown("# Spaces Configuration") - gr.Markdown("Create AI chat interfaces for HuggingFace Spaces. Configure your assistant and generate a deployment package.") - - with gr.Column(): - # Quick Start section with most common settings - with gr.Accordion("🚀 Quick Start", open=True): - gr.Markdown("Essential settings to get your assistant running quickly.") - - # Title and Description - single column at the top - title = gr.Textbox( + # Documentation Tab + with gr.Tab("Documentation"): + self._create_documentation_tab() + + return demo + + def _create_configuration_tab(self): + """Create the configuration tab with modern Gradio patterns""" + with gr.Column(): + # Template Selection + with gr.Group(): + gr.Markdown("### 📝 Quick Start Templates") + template_selector = gr.Dropdown( + label="Select Template", + choices=["None (Custom)"] + list(ACADEMIC_TEMPLATES.keys()), + value="None (Custom)", + interactive=True + ) + + # Space Identity + with gr.Group(): + gr.Markdown("### 🎯 Space Identity") + with gr.Row(): + self.name_input = gr.Textbox( label="Assistant Name", placeholder="My AI Assistant", - value="AI Assistant", - info="Display name for your assistant" - ) - - description = gr.Textbox( - label="Description", - placeholder="A helpful AI assistant for...", - value="A customizable AI assistant", - info="Brief description of your assistant's purpose" - ) - - model = gr.Dropdown( - label="Model", - choices=MODELS, - value=MODELS[0], - info="Choose the AI model that best fits your needs" + value="AI Assistant" ) - - # NEW: Gradio Theme selection - theme = gr.Dropdown( - label="Gradio Theme", - choices=[ - ("Default - Vibrant orange modern theme", "default"), - ("Origin - Classic Gradio 4 styling", "origin"), - ("Citrus - Yellow with 3D effects", "citrus"), - ("Monochrome - Black & white newspaper", "monochrome"), - ("Soft - Purple with rounded elements", "soft"), - ("Glass - Blue with translucent effects", "glass"), - ("Ocean - Ocean-inspired theme", "ocean") - ], - value="default", - info="Choose the visual theme for your chat interface" + self.theme_input = gr.Dropdown( + label="Theme", + choices=list(AVAILABLE_THEMES.keys()), + value="Default" ) - # Assistant Instructions section - with gr.Accordion("📝 Assistant Instructions", open=True): - gr.Markdown("Define your assistant's behavior and provide example prompts.") - - # Template selection moved to Assistant Instructions - template_selector = gr.Radio( - label="Assistant Template", - choices=["None (Custom)", "Research Template", "Socratic Template", "Mathematics Template"], - value="None (Custom)", - info="Start with a pre-configured template or create your own" - ) - - # Main system prompt field - system_prompt = gr.Textbox( - label="System Prompt", - placeholder="You are a helpful assistant that...", - lines=4, - value="", - info="Define the assistant's role and behavior. Auto-filled when you select a template above." - ) - - examples_text = gr.Textbox( - label="Example Prompts (one per line)", - placeholder="What can you help me with?\nExplain this concept in simple terms\nHelp me understand this topic", - lines=3, - info="These appear as clickable buttons in the chat interface" - ) + self.tagline_input = gr.Textbox( + label="Tagline", + placeholder="Brief tagline for HuggingFace...", + max_length=60, + info="Maximum 60 characters (for YAML frontmatter)" + ) - # URL Context section - closed by default to reduce intimidation - with gr.Accordion("🔗 URL Context (Optional)", open=False): - gr.Markdown("Add web pages to provide context for your assistant. Content will be automatically fetched and included.") - - # Primary URLs section - gr.Markdown("### 🎯 Primary URLs (Always Processed)") - gr.Markdown("These URLs are processed first and given highest priority for context.") - - url1 = gr.Textbox( - label="Primary URL 1", - placeholder="https://syllabus.edu/course (most important source)", - info="Main reference document, syllabus, or primary source" - ) - - url2 = gr.Textbox( - label="Primary URL 2", - placeholder="https://textbook.com/chapter (core material)", - info="Secondary reference, textbook chapter, or key resource" - ) - - # Secondary URLs section - gr.Markdown("### 📚 Secondary URLs (Additional Context)") - gr.Markdown("Additional sources for supplementary context and enhanced responses.") - - url3 = gr.Textbox( - label="Secondary URL 1", - placeholder="https://example.com/supplementary", - info="Additional reference or supplementary material", - visible=True - ) - - url4 = gr.Textbox( - label="Secondary URL 2", - placeholder="https://example.com/resources", - info="Extra context or supporting documentation", - visible=True - ) - - url5 = gr.Textbox( - label="Secondary URL 3", - placeholder="https://example.com/guidelines", - info="Additional guidelines or reference material", - visible=False - ) - - url6 = gr.Textbox( - label="Secondary URL 4", - placeholder="https://example.com/examples", - info="Examples, case studies, or additional sources", - visible=False - ) - - url7 = gr.Textbox( - label="Secondary URL 5", - placeholder="https://example.com/research", - info="Research papers or academic sources", - visible=False + self.description_input = gr.Textbox( + label="Description", + placeholder="A detailed description of your AI assistant. You can use markdown formatting here...", + lines=4, + info="Full markdown description for the README" + ) + + # System Configuration + with gr.Group(): + gr.Markdown("### ⚙️ System Configuration") + self.system_prompt_input = gr.Textbox( + label="System Prompt", + placeholder="You are a helpful AI assistant...", + lines=5 + ) + + self.model_input = gr.Dropdown( + label="Model", + choices=get_model_choices(), + value="google/gemini-2.0-flash-001" + ) + + with gr.Row(): + self.temperature_input = gr.Slider( + label="Temperature", + minimum=0, + maximum=2, + value=0.7, + step=0.1 ) - - url8 = gr.Textbox( - label="Secondary URL 6", - placeholder="https://example.com/documentation", - info="Technical documentation or specifications", - visible=False + self.max_tokens_input = gr.Slider( + label="Max Tokens", + minimum=50, + maximum=4096, + value=750, + step=50 ) - - url9 = gr.Textbox( - label="Secondary URL 7", - placeholder="https://example.com/articles", - info="Articles, blog posts, or news sources", - visible=False + + # Example Prompts + with gr.Group(): + gr.Markdown("### 💡 Example Prompts") + gr.Markdown("Provide 3-5 sample prompts that showcase your assistant's capabilities") + + # Create individual example input fields + self.example_inputs = [] + for i in range(5): + example_input = gr.Textbox( + label=f"Example {i+1}", + placeholder=f"Sample prompt {i+1}...", + visible=(i < 3) # Show first 3 by default ) - - url10 = gr.Textbox( - label="Secondary URL 8", - placeholder="https://example.com/misc", - info="Miscellaneous sources or background material", - visible=False + self.example_inputs.append(example_input) + + with gr.Row(): + add_example_btn = gr.Button("➕ Add Example", size="sm") + remove_example_btn = gr.Button("➖ Remove Example", size="sm", visible=False) + + self.example_count = gr.State(3) + + # URL Grounding + with gr.Group(): + gr.Markdown("### 🔗 URL Grounding") + gr.Markdown("Add URLs to provide context to your assistant") + + # Dynamic URL fields using gr.render + self.url_count = gr.State(2) + self.url_inputs = [] + + # Create initial URL inputs + for i in range(10): + url_input = gr.Textbox( + label=f"URL {i+1}" + (" (Primary)" if i < 2 else " (Secondary)"), + placeholder="https://...", + visible=(i < 2) ) - - # URL management buttons - with gr.Row(): - add_url_btn = gr.Button("+ Add Secondary URLs (2/8)", size="sm") - remove_url_btn = gr.Button("- Remove Secondary URLs", size="sm", visible=True) - url_count = gr.State(4) # Track number of visible URLs + self.url_inputs.append(url_input) - # Advanced Settings - includes technical configurations - with gr.Accordion("⚙️ Advanced Settings", open=False): - gr.Markdown("Fine-tune response behavior and configure access controls.") - - with gr.Row(): - temperature = gr.Slider( - label="Temperature", - minimum=0, - maximum=2, - value=0.7, - step=0.1, - info="Controls randomness: 0 = focused, 2 = creative" - ) - - max_tokens = gr.Slider( - label="Max Response Length", - minimum=50, - maximum=4096, - value=750, - step=50, - info="Maximum tokens (words) in each response" - ) - - - # Environment Variables Configuration - gr.Markdown("### 🔑 Space Secrets Configuration") - gr.Markdown("Configure these environment variables in your HuggingFace Space Settings → Variables and secrets") - - api_key_var = gr.Textbox( - label="API Key Variable (Required)", - value="API_KEY", - info="Environment variable name for your OpenRouter API key.", - interactive=False + with gr.Row(): + add_url_btn = gr.Button("➕ Add URL", size="sm") + remove_url_btn = gr.Button("➖ Remove URL", size="sm", visible=False) + + # API Configuration + with gr.Group(): + gr.Markdown("### 🔑 API Configuration") + gr.Markdown( + "Configure the required secrets in your HuggingFace Space settings." + ) + + # Required API Key on its own row + self.api_key_var_input = gr.Textbox( + label="API Key Variable Name (Required)", + value="API_KEY", + info="Environment variable for OpenRouter API key", + interactive=False # Make non-editable + ) + + # Optional variables on the same row + gr.Markdown("**Optional Environment Variables:**") + with gr.Row(): + self.hf_token_input = gr.Textbox( + label="HF Token Variable Name", + value="HF_TOKEN", + info="Environment variable for HuggingFace token", + interactive=False # Make non-editable ) - - access_code = gr.Textbox( - label="Student Access Code Variable (Optional)", + self.access_code_input = gr.Textbox( + label="Access Code Variable", value="ACCESS_CODE", - info="Environment variable for password-protecting the Space. Leave unset for public access.", - interactive=False - ) - - faculty_password = gr.Textbox( - label="Faculty Configuration Password Variable (Optional)", - value="CONFIG_CODE", - info="Environment variable for faculty to edit configuration after deployment. Enables live customization.", - interactive=False + info="Environment variable for password protection", + interactive=False # Make non-editable ) - with gr.Row(): - preview_btn = gr.Button("Preview Deployment Package", variant="secondary") - generate_btn = gr.Button("Generate Deployment Package", variant="primary") - - status = gr.Markdown(visible=False) - download_file = gr.File(label="Download your zip package", visible=False) + # Instructions with images + with gr.Accordion("📖 Step-by-Step Instructions", open=False): + gr.Markdown( + """**Step 1: Navigate to Settings** + + Click on the ⚙️ Settings tab in your HuggingFace Space: + + ![Settings Tab](../img/huggingface-settings-tab.png) + + **Step 2: Access Variables and Secrets** + + In the settings menu, you'll see a link to configure your secrets: + + ![Variables and Secrets](../img/huggingface-variables-secrets.png) + + **Step 3: Add Required Secrets** + + Add the following secrets to your Space: + + 1. **API_KEY** - Your OpenRouter API key (required) + - Get your key at: https://openrouter.ai/keys + - Value should start with `sk-or-` + + 2. **HF_TOKEN** - Your HuggingFace token (optional) + - Enables automatic configuration updates + - Get your token at: https://huggingface.co/settings/tokens + - Requires write permissions + + 3. **ACCESS_CODE** - Password protection (optional) + - Set any password to restrict access + - Share with authorized users only + """ + ) - # State variable to cache custom system prompt - custom_prompt_cache = gr.State("") + # Configuration Upload + with gr.Accordion("📤 Upload Configuration", open=False): + config_upload = gr.File( + label="Upload config.json", + file_types=[".json"], + type="filepath" + ) + upload_status = gr.Markdown(visible=False) + + # Action Buttons + with gr.Row(): + preview_btn = gr.Button("👁️ Preview Configuration", variant="secondary") + generate_btn = gr.Button("Generate Deployment Package", variant="primary") - # Connect the template selector + # Output Section + with gr.Column(visible=False) as output_section: + output_message = gr.Markdown() + download_file = gr.File(label="📦 Download Package", visible=False) + deployment_details = gr.Markdown(visible=False) + + # Event Handlers template_selector.change( - toggle_template, - inputs=[template_selector, system_prompt, custom_prompt_cache], - outputs=[system_prompt, custom_prompt_cache] + self._apply_template, + inputs=[ + template_selector, self.previous_template, self.template_cache, + self.name_input, self.tagline_input, self.description_input, self.system_prompt_input, + self.model_input, self.temperature_input, self.max_tokens_input + ] + self.example_inputs + self.url_inputs, + outputs=[ + self.name_input, self.tagline_input, self.description_input, self.system_prompt_input, + self.model_input, self.temperature_input, self.max_tokens_input + ] + self.example_inputs + self.url_inputs + [self.previous_template, self.template_cache] ) - # Web search checkbox is now just for enabling/disabling the feature - # No additional UI elements needed since we rely on model capabilities - + config_upload.upload( + self._apply_uploaded_config, + inputs=[config_upload], + outputs=[ + self.name_input, self.tagline_input, self.description_input, self.system_prompt_input, + self.model_input, self.theme_input, self.temperature_input, + self.max_tokens_input, upload_status + ] + self.example_inputs + self.url_inputs + ) + # URL management + def update_url_visibility(count): + new_count = min(count + 1, 10) + updates = [] + for i in range(10): + updates.append(gr.update(visible=(i < new_count))) + updates.append(gr.update(visible=(new_count > 2))) # Remove button + updates.append(new_count) + return updates + + def remove_url(count): + new_count = max(count - 1, 2) + updates = [] + for i in range(10): + updates.append(gr.update(visible=(i < new_count))) + updates.append(gr.update(visible=(new_count > 2))) # Remove button + updates.append(new_count) + return updates - # Connect the URL management buttons add_url_btn.click( - add_urls, - inputs=[url_count], - outputs=[url3, url4, url5, url6, url7, url8, url9, url10, add_url_btn, remove_url_btn, url_count] + update_url_visibility, + inputs=[self.url_count], + outputs=self.url_inputs + [remove_url_btn, self.url_count] ) remove_url_btn.click( - remove_urls, - inputs=[url_count], - outputs=[url3, url4, url5, url6, url7, url8, url9, url10, add_url_btn, remove_url_btn, url_count] + remove_url, + inputs=[self.url_count], + outputs=self.url_inputs + [remove_url_btn, self.url_count] ) - + # Example management + def update_example_visibility(count): + new_count = min(count + 1, 5) + updates = [] + for i in range(5): + updates.append(gr.update(visible=(i < new_count))) + updates.append(gr.update(visible=(new_count > 3))) # Remove button + updates.append(new_count) + return updates + + def remove_example(count): + new_count = max(count - 1, 3) + updates = [] + for i in range(5): + updates.append(gr.update(visible=(i < new_count))) + updates.append(gr.update(visible=(new_count > 3))) # Remove button + updates.append(new_count) + return updates + + add_example_btn.click( + update_example_visibility, + inputs=[self.example_count], + outputs=self.example_inputs + [remove_example_btn, self.example_count] + ) - # Connect the generate button - generate_btn.click( - on_generate, - inputs=[title, description, system_prompt, model, theme, api_key_var, temperature, max_tokens, examples_text, access_code, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10], - outputs=[status, download_file, sandbox_state] + remove_example_btn.click( + remove_example, + inputs=[self.example_count], + outputs=self.example_inputs + [remove_example_btn, self.example_count] ) - - with gr.Tab("Preview"): - gr.Markdown("# Sandbox Preview") - gr.Markdown("Test your assistant before deployment.") + # Preview and Generate handlers + preview_btn.click( + self._preview_configuration, + inputs=[ + self.name_input, self.tagline_input, self.description_input, self.system_prompt_input, + self.model_input, self.theme_input, self.api_key_var_input, + self.temperature_input, self.max_tokens_input, self.access_code_input + ] + self.example_inputs + self.url_inputs, + outputs=[self.config_state] + ).then( + lambda: gr.Tabs(selected=1), # Switch to preview tab + outputs=[self.main_tabs] + ) + + generate_btn.click( + self._generate_package, + inputs=[ + self.name_input, self.tagline_input, self.description_input, self.system_prompt_input, + self.model_input, self.theme_input, self.api_key_var_input, + self.temperature_input, self.max_tokens_input, self.access_code_input + ] + self.example_inputs + self.url_inputs, + outputs=[ + output_section, output_message, download_file, + deployment_details, self.config_state + ] + ) + + def _create_preview_tab(self): + """Create the preview tab with modern patterns""" + with gr.Column(): + # Preview info + preview_info = gr.Markdown( + "Configure your assistant in the Configuration tab and click 'Preview Configuration' to test it here.", + visible=True + ) - with gr.Column(): - # Preview status - assign to global variable - preview_status_comp = gr.Markdown("**Status:** Configure your space in the Configuration tab and click 'Preview Deployment Package' to see your assistant here.", visible=True) + # Preview interface (hidden initially) + with gr.Column(visible=False) as preview_interface: + gr.Markdown("### Assistant Preview") - # Simulated chat interface for preview using ChatInterface - with gr.Column(visible=False) as preview_chat_section_comp: + # Use gr.render for dynamic preview based on config + @gr.render(inputs=[self.config_state]) + def render_preview(config): + if not config or not config.get('preview_ready'): + return + + # Preview chatbot preview_chatbot = gr.Chatbot( - value=[], - label="Preview Chat Interface", + label=config.get('name', 'AI Assistant'), height=400, - type="messages" + show_copy_button=True, + bubble_full_width=False ) - # Example prompt buttons + + # Example buttons + examples = config.get('examples_list', []) + if examples: + gr.Markdown("**Try these examples:**") + example_btns = [] + for i, example in enumerate(examples[:3]): + btn = gr.Button(f"{example}", size="sm") + example_btns.append((btn, example)) + + # Chat input with gr.Row(): - preview_example_btn1 = gr.Button("", visible=False, size="sm", variant="secondary") - preview_example_btn2 = gr.Button("", visible=False, size="sm", variant="secondary") - preview_example_btn3 = gr.Button("", visible=False, size="sm", variant="secondary") + preview_input = gr.Textbox( + label="Message", + placeholder="Type your message...", + lines=1, + scale=4 + ) + preview_send = gr.Button("Send", variant="primary", scale=1) - preview_msg = gr.Textbox( - label="Test your assistant", - placeholder="Type a message to test your assistant...", - lines=2 - ) + # Export functionality + with gr.Row(): + preview_clear = gr.Button("🗑️ Clear", size="sm") + preview_export = gr.DownloadButton( + "Export Conversation", + size="sm", + visible=False + ) - # URL context fields for preview testing - with gr.Accordion("Test URL Context (Optional)", open=False): - gr.Markdown("Test URL context grounding in the preview. Uses same priority system: 2 primary + 8 secondary URLs.") - - # Primary URLs for preview testing - gr.Markdown("**🎯 Primary URLs**") - with gr.Row(): - preview_url1 = gr.Textbox( - label="Primary URL 1", - placeholder="https://syllabus.edu/course", - scale=1 - ) - preview_url2 = gr.Textbox( - label="Primary URL 2", - placeholder="https://textbook.com/chapter", - scale=1 - ) + # Configuration display + with gr.Accordion("Configuration Details", open=False): + config_display = gr.Markdown(self._format_config_display(config)) + + # Event handlers + def preview_respond(message, history): + # Simulate response for preview + api_key = os.environ.get(config.get('api_key_var', 'API_KEY')) - # Secondary URLs for preview testing - gr.Markdown("**📚 Secondary URLs**") - with gr.Row(): - preview_url3 = gr.Textbox( - label="Secondary URL 1", - placeholder="https://example.com/supplementary", - scale=1, - visible=True + if not api_key: + response = ( + f"🔑 **API Key Required for Preview**\n\n" + f"To test with real responses, set your `{config.get('api_key_var', 'API_KEY')}` " + f"environment variable.\n\n" + f"**Configured Model:** {config.get('model', 'Unknown')}" ) - preview_url4 = gr.Textbox( - label="Secondary URL 2", - placeholder="https://example.com/resources", - scale=1, - visible=True + else: + # Here you would make actual API call + response = ( + f"[Preview Mode] This would call {config.get('model', 'Unknown')} " + f"with your message: '{message}'" ) - with gr.Row(): - preview_url5 = gr.Textbox( - label="Secondary URL 3", - placeholder="https://example.com/guidelines", - scale=1, - visible=False - ) - preview_url6 = gr.Textbox( - label="Secondary URL 4", - placeholder="https://example.com/examples", - scale=1, - visible=False + history.append({"role": "user", "content": message}) + history.append({"role": "assistant", "content": response}) + return "", history + + preview_send.click( + preview_respond, + inputs=[preview_input, preview_chatbot], + outputs=[preview_input, preview_chatbot] + ) + + preview_input.submit( + preview_respond, + inputs=[preview_input, preview_chatbot], + outputs=[preview_input, preview_chatbot] + ) + + preview_clear.click( + lambda: ("", []), + outputs=[preview_input, preview_chatbot] + ) + + # Example button handlers + if examples: + for btn, example in example_btns: + btn.click( + lambda ex=example: ex, + outputs=[preview_input] ) + + # Export handler + def prepare_export(history): + if not history: + return gr.update(visible=False) - with gr.Row(): - preview_url7 = gr.Textbox( - label="Secondary URL 5", - placeholder="https://example.com/research", - scale=1, - visible=False - ) - preview_url8 = gr.Textbox( - label="Secondary URL 6", - placeholder="https://example.com/documentation", - scale=1, - visible=False - ) + content = export_conversation_to_markdown(history, config) + filename = create_safe_filename( + config.get('name', 'assistant'), + prefix="preview" + ) - with gr.Row(): - preview_url9 = gr.Textbox( - label="Secondary URL 7", - placeholder="https://example.com/articles", - scale=1, - visible=False - ) - preview_url10 = gr.Textbox( - label="Secondary URL 8", - placeholder="https://example.com/misc", - scale=1, - visible=False - ) + temp_file = Path(f"/tmp/{filename}") + temp_file.write_text(content) - # URL management for preview - with gr.Row(): - preview_add_url_btn = gr.Button("+ Add Secondary URLs (2/8)", size="sm") - preview_remove_url_btn = gr.Button("- Remove Secondary URLs", size="sm", visible=True) - preview_url_count = gr.State(4) - - with gr.Row(): - preview_send = gr.Button("Send", variant="primary") - preview_clear = gr.Button("Clear") - preview_export_btn = gr.Button("Export Conversation", variant="secondary") + return gr.update( + visible=True, + value=str(temp_file) + ) - # Export functionality - export_file = gr.File(label="Download Conversation", visible=False) + preview_chatbot.change( + prepare_export, + inputs=[preview_chatbot], + outputs=[preview_export] + ) + + def _create_documentation_tab(self): + """Create the documentation tab""" + with gr.Column(): + gr.Markdown(self._get_support_docs()) + + def _apply_template(self, template_name, prev_template, cache, + name, tagline, desc, prompt, model, temp, tokens, *args): + """Apply selected template to form fields with caching""" + # Split args into examples and URLs + example_values = args[:5] # First 5 are examples + url_values = args[5:] # Rest are URLs + + # First, cache the current values if switching from custom + if prev_template == "None (Custom)" and template_name != "None (Custom)": + # Cache custom values - collect non-empty examples and URLs + examples_list = [ex for ex in example_values if ex and ex.strip()] + urls_list = [url for url in url_values if url and url.strip()] + cache["custom"] = { + 'name': name, + 'tagline': tagline, + 'description': desc, + 'system_prompt': prompt, + 'model': model, + 'temperature': temp, + 'max_tokens': tokens, + 'examples': examples_list, + 'grounding_urls': urls_list + } + + # Apply new template values + if template_name == "None (Custom)": + # Restore custom values if they exist + if "custom" in cache: + custom = cache["custom"] + cached_examples = custom.get('examples', []) + cached_urls = custom.get('grounding_urls', []) + + # Prepare example updates - fill first 5 fields + example_updates = [] + for i in range(5): + if i < len(cached_examples): + example_updates.append(gr.update(value=cached_examples[i])) + else: + example_updates.append(gr.update(value="")) + + # Prepare URL updates - fill first 10 fields + url_updates = [] + for i in range(10): + if i < len(cached_urls): + url_updates.append(gr.update(value=cached_urls[i])) + else: + url_updates.append(gr.update(value="")) - # Configuration display - assign to global variable - config_display_comp = gr.Markdown("Configuration will appear here after preview generation.") + return [ + gr.update(value=custom.get('name', '')), + gr.update(value=custom.get('tagline', '')), + gr.update(value=custom.get('description', '')), + gr.update(value=custom.get('system_prompt', '')), + gr.update(value=custom.get('model', 'google/gemini-2.0-flash-001')), + gr.update(value=custom.get('temperature', 0.7)), + gr.update(value=custom.get('max_tokens', 750)) + ] + example_updates + url_updates + [template_name, cache] + else: + # No cached values, return defaults + default_examples = [ + "Hello! How can you help me?", + "Tell me something interesting", + "What can you do?", + "", + "" + ] + example_updates = [gr.update(value=ex) for ex in default_examples] + url_updates = [gr.update(value="") for _ in range(10)] + return [ + gr.update(value='AI Assistant'), + gr.update(value='A customizable AI assistant'), + gr.update(value='A versatile AI assistant powered by advanced language models.'), + gr.update(value='You are a helpful AI assistant.'), + gr.update(value='google/gemini-2.0-flash-001'), + gr.update(value=0.7), + gr.update(value=750) + ] + example_updates + url_updates + [template_name, cache] + + elif template_name in ACADEMIC_TEMPLATES: + template = ACADEMIC_TEMPLATES[template_name] + template_examples = template.get('examples', []) + template_urls = template.get('grounding_urls', []) + + # Prepare example updates - fill available examples, empty the rest + example_updates = [] + for i in range(5): + if i < len(template_examples): + example_updates.append(gr.update(value=template_examples[i])) + else: + example_updates.append(gr.update(value="")) + # Prepare URL updates - fill available URLs, empty the rest + url_updates = [] + for i in range(10): + if i < len(template_urls): + url_updates.append(gr.update(value=template_urls[i])) + else: + url_updates.append(gr.update(value="")) + + return [ + gr.update(value=template.get('name', '')), + gr.update(value=template.get('tagline', template.get('description', '')[:60])), + gr.update(value=template.get('description', '')), + gr.update(value=template.get('system_prompt', '')), + gr.update(value=template.get('model', 'google/gemini-2.0-flash-001')), + gr.update(value=template.get('temperature', 0.7)), + gr.update(value=template.get('max_tokens', 750)) + ] + example_updates + url_updates + [template_name, cache] + else: + # Invalid template, no updates + # 7 basic fields + 5 examples + 10 URLs = 22, plus prev_template and cache = 24 total + return [gr.update() for _ in range(22)] + [prev_template, cache] + + def _apply_uploaded_config(self, config_file): + """Apply uploaded configuration file""" + if not config_file: + # 8 basic + 1 status + 5 examples + 10 URLs = 24 total + return [gr.update() for _ in range(24)] + + try: + with open(config_file, 'r') as f: + config = json.load(f) + + # Extract values + updates = [ + gr.update(value=config.get('name', '')), + gr.update(value=config.get('tagline', config.get('description', '')[:60])), + gr.update(value=config.get('description', '')), + gr.update(value=config.get('system_prompt', '')), + gr.update(value=config.get('model', 'google/gemini-2.0-flash-001')), + gr.update(value=config.get('theme', 'Default')), + gr.update(value=config.get('temperature', 0.7)), + gr.update(value=config.get('max_tokens', 750)) + ] - # Connect preview chat functionality - preview_send.click( - preview_chat_response, - inputs=[preview_msg, preview_chatbot, preview_config_state, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10], - outputs=[preview_msg, preview_chatbot] - ) + # Status message + updates.append(gr.update( + value=f"Configuration loaded successfully", + visible=True + )) + + # Example updates + examples = config.get('examples', []) + for i in range(5): + if i < len(examples): + updates.append(gr.update(value=examples[i])) + else: + updates.append(gr.update(value="")) - preview_msg.submit( - preview_chat_response, - inputs=[preview_msg, preview_chatbot, preview_config_state, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10], - outputs=[preview_msg, preview_chatbot] - ) + # URL updates + urls = config.get('grounding_urls', []) + for i in range(10): + if i < len(urls): + updates.append(gr.update(value=urls[i])) + else: + updates.append(gr.update(value="")) + + return updates + + except Exception as e: + error_updates = [gr.update() for _ in range(8)] # Basic fields + error_updates.append(gr.update( + value=f"Error loading configuration: {str(e)}", + visible=True + )) + error_updates.extend([gr.update() for _ in range(5)]) # Examples + error_updates.extend([gr.update() for _ in range(10)]) # URLs + return error_updates + + def _preview_configuration(self, name, tagline, description, system_prompt, model, + theme, api_key_var, temperature, max_tokens, + access_code, *args): + """Preview the configuration""" + # Split args into examples and URLs + example_values = args[:5] # First 5 are examples + urls = args[5:] # Rest are URLs + + # Build configuration + config = { + 'name': name or 'AI Assistant', + 'tagline': tagline or 'A customizable AI assistant', + 'description': description or 'A versatile AI assistant powered by advanced language models.', + 'system_prompt': system_prompt or 'You are a helpful AI assistant.', + 'model': model, + 'theme': theme, + 'api_key_var': api_key_var, + 'temperature': temperature, + 'max_tokens': int(max_tokens), + 'access_code': access_code, + 'grounding_urls': [url for url in urls if url and url.strip()], + 'examples_list': [ex.strip() for ex in example_values if ex and hasattr(ex, 'strip') and ex.strip()], + 'preview_ready': True + } + + gr.Info("Preview updated! Switch to the Preview tab to test your assistant.") + return config + + def _generate_package(self, name, tagline, description, system_prompt, model, + theme, api_key_var, temperature, max_tokens, + access_code, *args): + """Generate the deployment package""" + try: + # Validate inputs + if not system_prompt: + gr.Error("Please provide a system prompt") + return gr.update(), gr.update(), gr.update(), gr.update(), {} + + # Split args into examples and URLs + example_values = args[:5] # First 5 are examples + urls = args[5:] # Rest are URLs + + # Process examples + examples_list = [ex.strip() for ex in example_values if ex and hasattr(ex, 'strip') and ex.strip()] + examples_python = repr(examples_list) + + # Process URLs + grounding_urls = [url.strip() for url in urls if url and hasattr(url, 'strip') and url.strip()] + + # Create configuration + config = { + 'name': repr(name or 'AI Assistant'), + 'description': repr(tagline or 'A customizable AI assistant'), + 'system_prompt': repr(system_prompt), + 'model': repr(model), + 'api_key_var': repr(api_key_var), + 'temperature': temperature, + 'max_tokens': int(max_tokens), + 'examples': examples_python, + 'grounding_urls': json.dumps(grounding_urls), + 'enable_dynamic_urls': True, + 'enable_file_upload': True, + 'theme': repr(theme) + } - preview_clear.click( - clear_preview_chat, - outputs=[preview_msg, preview_chatbot] - ) + # Generate files + template = get_template() + app_content = template.format(**config) + + requirements_content = """gradio>=5.39.0 +requests>=2.32.3 +beautifulsoup4>=4.12.3 +python-dotenv>=1.0.0 +huggingface-hub>=0.20.0""" + + config_json = { + 'name': name or 'AI Assistant', + 'tagline': tagline or 'A customizable AI assistant', + 'description': description or 'A versatile AI assistant powered by advanced language models.', + 'system_prompt': system_prompt, + 'model': model, + 'api_key_var': api_key_var, + 'temperature': temperature, + 'max_tokens': int(max_tokens), + 'examples': examples_list, + 'grounding_urls': grounding_urls, + 'enable_dynamic_urls': True, + 'enable_file_upload': True, + 'theme': theme + } - preview_export_btn.click( - export_preview_conversation, - inputs=[preview_chatbot], - outputs=[export_file] + # Create README + readme_content = self._create_readme( + name or 'AI Assistant', + tagline or 'A customizable AI assistant', + description or 'A versatile AI assistant powered by advanced language models. Configure it to meet your specific needs with custom prompts, examples, and grounding URLs.', + model, + api_key_var, + access_code ) - # Connect preview URL management buttons - preview_add_url_btn.click( - add_urls, - inputs=[preview_url_count], - outputs=[preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count] - ) + # Create zip file + filename = create_safe_filename(name or 'ai_assistant', suffix='.zip') - preview_remove_url_btn.click( - remove_urls, - inputs=[preview_url_count], - outputs=[preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count] - ) + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + zip_file.writestr('app.py', app_content) + zip_file.writestr('requirements.txt', requirements_content) + zip_file.writestr('config.json', json.dumps(config_json, indent=2)) + zip_file.writestr('README.md', readme_content) - # Connect example buttons to populate text input - # Need to get the example text from the state - def get_example_text(config_data, index): - if config_data and config_data.get('examples_list'): - examples = config_data['examples_list'] - if index < len(examples): - return examples[index] - return "" + # Save zip file + zip_buffer.seek(0) + with open(filename, 'wb') as f: + f.write(zip_buffer.getvalue()) - preview_example_btn1.click( - lambda config_data: get_example_text(config_data, 0), - inputs=[preview_config_state], - outputs=[preview_msg] - ) + # Success message + title_msg = f"**🎉 Deployment package ready!**\n\n**File**: `{filename}`" + + details_msg = f"""**Package Contents:** +- `app.py` - Ready-to-deploy Gradio application +- `requirements.txt` - Python dependencies +- `config.json` - Configuration backup +- `README.md` - Deployment instructions + +**Next Steps:** +1. Download the package below +2. Create a new HuggingFace Space +3. Upload all files from the package +4. Set your `{api_key_var}` secret in Space settings""" - preview_example_btn2.click( - lambda config_data: get_example_text(config_data, 1), - inputs=[preview_config_state], - outputs=[preview_msg] + if access_code: + details_msg += f"\n5. Set your `ACCESS_CODE` secret for access control" + + return ( + gr.update(visible=True), + gr.update(value=title_msg, visible=True), + gr.update(value=filename, visible=True), + gr.update(value=details_msg, visible=True), + config_json ) - preview_example_btn3.click( - lambda config_data: get_example_text(config_data, 2), - inputs=[preview_config_state], - outputs=[preview_msg] + except Exception as e: + return ( + gr.update(visible=True), + gr.update(value=f"Error: {str(e)}", visible=True), + gr.update(visible=False), + gr.update(visible=False), + {} ) - - with gr.Tab("Support"): - create_support_docs() - # Connect cross-tab functionality after all components are defined - preview_btn.click( - on_preview_combined, - inputs=[title, description, system_prompt, model, theme, temperature, max_tokens, examples_text, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10], - outputs=[preview_config_state, preview_status_comp, preview_chat_section_comp, config_display_comp, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count, preview_example_btn1, preview_example_btn2, preview_example_btn3] - ) + def _format_config_display(self, config): + """Format configuration for display""" + return f"""**Model:** {config.get('model', 'Not set')} +**Temperature:** {config.get('temperature', 0.7)} +**Max Tokens:** {config.get('max_tokens', 750)} +**Theme:** {config.get('theme', 'Default')} + +**System Prompt:** +{config.get('system_prompt', 'Not set')}""" + + def _get_support_docs(self): + """Get support documentation content""" + return """## ChatUI Helper Workflow + +### Step 1: Configure Your Space +**Configuration Tab** provides these sections: +1. **📝 Quick Start Templates** - Choose from academic templates or start custom +2. **🎯 Space Identity** - Set name, description, and theme +3. **⚙️ System Configuration** - Define system prompt, model, and parameters +4. **💡 Example Prompts** - Add sample interactions for users +5. **🔗 URL Grounding** - Add context URLs (up to 10, first 2 are primary) +6. **🔑 API Configuration** - Set required environment variables +7. **📤 Upload Configuration** - Import existing config.json files + +### Step 2: Preview Your Assistant +**Preview Tab** allows you to: +- Test your configuration in real-time +- Try example prompts +- Export conversation history +- Validate API connections + +### Step 3: Generate Deployment Package +Click **Generate Deployment Package** to create: +- `app.py` - Complete Gradio application +- `requirements.txt` - Dependencies +- `config.json` - Configuration backup +- `README.md` - Setup instructions + +### Required Secrets Setup +In your HuggingFace Space settings: +1. **API_KEY** (Required) - Your OpenRouter API key from https://openrouter.ai/keys +2. **HF_TOKEN** (Optional) - Enables auto-configuration updates +3. **ACCESS_CODE** (Optional) - Password protection + +### Template System +- Switch between **None (Custom)** and academic templates +- Custom values are preserved when switching templates +- Templates include STEM, Business, Creative Writing, etc. + +### Troubleshooting +- **Build errors**: Check requirements.txt compatibility +- **API errors**: Verify API_KEY is set correctly +- **Access issues**: Ensure ACCESS_CODE matches configuration +- **Template issues**: Custom values are cached when switching""" + + def _create_readme(self, title, tagline, description, model, api_key_var, access_code): + """Create README.md content""" + emoji = "💬" + + + access_section = "" + if access_code: + access_section = f""" +### Step 3: Set Access Code +1. In Settings → Variables and secrets +2. Add secret: `ACCESS_CODE` +3. Set your chosen password +4. Share with authorized users +""" + + return f"""--- +title: {title} +emoji: {emoji} +colorFrom: blue +colorTo: green +sdk: gradio +sdk_version: 5.39.0 +app_file: app.py +pinned: false +license: mit +short_description: {tagline} +--- + +# {title} + +{description} + +## Quick Setup + +### Step 1: Configure API Key (Required) +1. Get your API key from https://openrouter.ai/keys +2. In Settings → Variables and secrets +3. Add secret: `{api_key_var}` +4. Paste your OpenRouter API key + +### Step 2: Configure HuggingFace Token (Optional) +1. Get your token from https://huggingface.co/settings/tokens +2. In Settings → Variables and secrets +3. Add secret: `HF_TOKEN` +4. Paste your HuggingFace token (needs write permissions) +5. This enables automatic configuration updates + +{access_section} + +### Step 3: Test Your Space +Your Space should now be running! Try the example prompts or ask your own questions. + +## Configuration +- **Model**: {model} +- **API Key Variable**: {api_key_var} +- **HF Token Variable**: HF_TOKEN (for auto-updates) +{f"- **Access Control**: Enabled (ACCESS_CODE)" if access_code else "- **Access**: Public"} + +## Support +For help, visit the HuggingFace documentation or community forums.""" + + +def main(): + """Main entry point""" + generator = SpaceGenerator() + demo = generator.create_interface() + demo.launch(share=True) + if __name__ == "__main__": - # Use default Gradio launch settings for HuggingFace Spaces compatibility - demo.launch(share=True) \ No newline at end of file + main() \ No newline at end of file