import gradio as gr from groq import Groq from fpdf import FPDF import json import os import re from datetime import datetime # Initialize Groq client from environment variable GROQ_API_KEY = os.environ.get("GROQ_API_KEY") if not GROQ_API_KEY: raise ValueError("GROQ_API_KEY not found in environment variables. Please set it in Space Secrets.") client = Groq(api_key=GROQ_API_KEY) def clean_json_string(content): """Remove invalid control characters from JSON string""" content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]', '', content) return content def generate_roadmap(domain, level, time_available, time_unit): """Generate roadmap using Groq API""" if time_unit == "Weeks": total_weeks = int(time_available) elif time_unit == "Months": total_weeks = int(time_available) * 4 elif time_unit == "Years": total_weeks = int(time_available) * 52 system_prompt = """You are an expert educational curriculum designer. Return only valid JSON without any markdown formatting or explanatory text.""" user_prompt = f"""Create a detailed learning roadmap for {domain} at {level} level. Duration: {time_available} {time_unit.lower()} ({total_weeks} weeks). STRICT REQUIREMENTS: 1. Return ONLY valid JSON, no markdown, no backticks, no explanation 2. Use this exact structure: {{ "domain": "{domain}", "level": "{level}", "duration": "{time_available} {time_unit}", "overview": "Brief overview text here", "phases": [ {{ "phase_name": "Phase Name", "duration_weeks": number, "description": "Description here", "topics": [ {{ "name": "Topic name", "resources": ["resource 1", "resource 2"], "practice_project": "Project description" }} ], "milestones": ["milestone 1", "milestone 2"] }} ], "essential_resources": {{ "courses": ["course 1"], "books": ["book 1"], "communities": ["community 1"], "tools": ["tool 1"] }}, "tips_for_success": ["tip 1", "tip 2"], "next_steps": "What to do next" }} 3. Ensure all strings are properly escaped (no raw newlines or tabs in strings) 4. Use \\n for newlines within strings if needed""" try: response = client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.7, max_tokens=4096 ) content = response.choices[0].message.content.strip() # Clean up markdown code blocks if content.startswith("```json"): content = content[7:] elif content.startswith("```"): content = content[3:] if content.endswith("```"): content = content[:-3] content = content.strip() content = clean_json_string(content) # Try to parse JSON try: data = json.loads(content) return data except json.JSONDecodeError as e: # Try to fix common issues content = re.sub(r'(".*?)\n(.*?")', r'\1\\n\2', content, flags=re.DOTALL) content = re.sub(r'(".*?)\t(.*?")', r'\1\\t\2', content, flags=re.DOTALL) data = json.loads(content) return data except Exception as e: return {"error": f"Failed to generate roadmap: {str(e)}"} def format_roadmark_for_display(roadmap_data): """Format roadmap as HTML""" if "error" in roadmap_data: return f"

{roadmap_data['error']}

", None try: html_output = f"""

🎯 {roadmap_data.get('domain', 'Unknown')} Roadmap

Level: {roadmap_data.get('level', 'N/A')} | Duration: {roadmap_data.get('duration', 'N/A')}

Overview

{roadmap_data.get('overview', 'No overview provided').replace(chr(10), '
')}

""" for i, phase in enumerate(roadmap_data.get('phases', []), 1): html_output += f"""

Phase {i}: {phase.get('phase_name', 'Unnamed')}

Duration: {phase.get('duration_weeks', 'N/A')} weeks

{phase.get('description', '').replace(chr(10), '
')}

Topics:

""" for topic in phase.get('topics', []): html_output += f"
" html_output += f"

{topic.get('name', 'Unnamed')}

" html_output += f"
    " for res in topic.get('resources', []): html_output += f"
  • {res}
  • " html_output += f"
" html_output += f"

Project: {topic.get('practice_project', 'N/A')}

" html_output += f"
" if phase.get('milestones'): html_output += f"

Milestones:

    " for ms in phase['milestones']: html_output += f"
  • ☐ {ms}
  • " html_output += f"
" html_output += "
" html_output += "
" return html_output, json.dumps(roadmap_data) except Exception as e: return f"

Error formatting display: {str(e)}

", None def generate_pdf(roadmap_data, domain): """Generate PDF file""" try: if isinstance(roadmap_data, str): roadmap_data = json.loads(roadmap_data) if not roadmap_data or "error" in roadmap_data: return None pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=15) def clean(text): if text is None: return "" text = str(text) return text.encode('latin-1', 'replace').decode('latin-1') # Title Page pdf.add_page() pdf.set_font("Arial", 'B', 24) pdf.set_text_color(102, 126, 234) pdf.cell(0, 20, clean(f"Learning Roadmap: {roadmap_data.get('domain', domain)}"), ln=True, align='C') pdf.set_font("Arial", '', 12) pdf.set_text_color(100, 100, 100) pdf.cell(0, 10, clean(f"Level: {roadmap_data.get('level', 'N/A')} | Duration: {roadmap_data.get('duration', 'N/A')}"), ln=True, align='C') pdf.ln(10) # Overview pdf.set_font("Arial", 'B', 16) pdf.set_text_color(0, 0, 0) pdf.cell(0, 10, "Overview", ln=True) pdf.set_font("Arial", '', 11) pdf.multi_cell(0, 6, clean(roadmap_data.get('overview', 'No overview'))) pdf.ln(5) # Phases for i, phase in enumerate(roadmap_data.get('phases', []), 1): pdf.add_page() pdf.set_font("Arial", 'B', 18) pdf.set_fill_color(102, 126, 234) pdf.set_text_color(255, 255, 255) pdf.cell(0, 12, clean(f"Phase {i}: {phase.get('phase_name', f'Phase {i}')}"), ln=True, fill=True) pdf.set_text_color(100, 100, 100) pdf.set_font("Arial", 'I', 11) pdf.cell(0, 8, f"Duration: {phase.get('duration_weeks', 'N/A')} weeks", ln=True) pdf.ln(2) pdf.set_text_color(0, 0, 0) pdf.set_font("Arial", '', 11) pdf.multi_cell(0, 6, clean(phase.get('description', ''))) pdf.ln(5) pdf.set_font("Arial", 'B', 13) pdf.set_text_color(102, 126, 234) pdf.cell(0, 8, "Topics & Resources:", ln=True) for topic in phase.get('topics', []): pdf.set_font("Arial", 'B', 11) pdf.set_text_color(0, 0, 0) pdf.cell(0, 6, clean(f"- {topic.get('name', 'Topic')}"), ln=True) pdf.set_font("Arial", '', 10) pdf.set_text_color(80, 80, 80) for resource in topic.get('resources', []): pdf.cell(10) pdf.cell(0, 5, clean(f" * {resource}"), ln=True) pdf.set_text_color(200, 100, 0) pdf.set_font("Arial", 'B', 10) pdf.cell(0, 5, clean(f"Project: {topic.get('practice_project', 'N/A')}"), ln=True) pdf.ln(3) if phase.get('milestones'): pdf.set_font("Arial", 'B', 12) pdf.set_text_color(40, 167, 69) pdf.cell(0, 8, "Milestones:", ln=True) pdf.set_font("Arial", '', 10) pdf.set_text_color(0, 0, 0) for milestone in phase.get('milestones', []): pdf.cell(10) pdf.cell(0, 5, clean(f"[ ] {milestone}"), ln=True) pdf.ln(5) # Resources if roadmap_data.get('essential_resources'): pdf.add_page() pdf.set_font("Arial", 'B', 18) pdf.set_fill_color(102, 126, 234) pdf.set_text_color(255, 255, 255) pdf.cell(0, 12, "Essential Resources", ln=True, fill=True) pdf.ln(5) resources = roadmap_data['essential_resources'] sections = [ ("Recommended Courses", resources.get('courses', [])), ("Books", resources.get('books', [])), ("Communities", resources.get('communities', [])), ("Tools", resources.get('tools', [])) ] for title, items in sections: if items: pdf.set_font("Arial", 'B', 13) pdf.set_text_color(102, 126, 234) pdf.cell(0, 8, clean(title), ln=True) pdf.set_font("Arial", '', 10) pdf.set_text_color(0, 0, 0) for item in items: pdf.cell(5) pdf.cell(0, 5, clean(f"- {item}"), ln=True) pdf.ln(3) # Save to /tmp for Hugging Face safe_domain = "".join(c for c in domain if c.isalnum() or c in (' ', '-', '_')).rstrip().replace(' ', '_') or "roadmap" filename = f"roadmap_{safe_domain}_{datetime.now().strftime('%Y%m%d')}.pdf" filepath = os.path.join("/tmp", filename) pdf.output(filepath) return filepath except Exception as e: print(f"PDF Error: {e}") return None # Build Gradio Interface def create_app(): with gr.Blocks(title="AI Learning Roadmap Generator") as app: roadmap_json = gr.State("") domain_name = gr.State("") gr.Markdown("# 🎯 AI Learning Roadmap Generator") gr.Markdown("Generate personalized learning paths for any skill domain") with gr.Row(): with gr.Column(scale=1): domain_input = gr.Textbox(label="Learning Domain", placeholder="e.g., Machine Learning, Web Development...") level_input = gr.Dropdown(["Beginner", "Intermediate", "Advanced"], label="Level", value="Beginner") with gr.Row(): time_input = gr.Number(value=3, minimum=1, label="Time") time_unit = gr.Dropdown(["Weeks", "Months", "Years"], value="Months", label="Unit") generate_btn = gr.Button("🚀 Generate Roadmap", variant="primary") gr.Examples( [["Machine Learning", "Beginner", 6, "Months"], ["Web Development", "Intermediate", 4, "Months"], ["Python Programming", "Beginner", 8, "Weeks"]], inputs=[domain_input, level_input, time_input, time_unit] ) with gr.Column(scale=2): html_output = gr.HTML(label="Your Roadmap") with gr.Row(): download_btn = gr.Button("📄 Download PDF", variant="primary", visible=False) pdf_output = gr.File(label="Your PDF", interactive=False, visible=False) def generate(domain, level, time_val, unit): if not domain or not domain.strip(): return "Please enter a domain", gr.update(visible=False), gr.update(visible=False), "", "" result = generate_roadmap(domain, level, time_val, unit) if "error" in result: return f"

❌ {result['error']}

", gr.update(visible=False), gr.update(visible=False), "", "" html, json_str = format_roadmark_for_display(result) if json_str is None: return html, gr.update(visible=False), gr.update(visible=False), "", "" return html, gr.update(visible=True), gr.update(visible=True), json_str, domain def download(json_data, dom): if not json_data: return None return generate_pdf(json_data, dom) generate_btn.click( fn=generate, inputs=[domain_input, level_input, time_input, time_unit], outputs=[html_output, download_btn, pdf_output, roadmap_json, domain_name] ) download_btn.click( fn=download, inputs=[roadmap_json, domain_name], outputs=pdf_output ) return app app = create_app() app.launch()