Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Docs Navigator MCP - Modern Gradio UI | |
| An elegant, AI-powered documentation assistant with advanced features. | |
| """ | |
| import os | |
| import gradio as gr | |
| from pathlib import Path | |
| from typing import List, Dict, Optional | |
| from anthropic import Anthropic | |
| from dotenv import load_dotenv | |
| from document_intelligence import DocumentIntelligence | |
| import time | |
| # Load environment variables | |
| load_dotenv() | |
| # Configuration | |
| DOCS_DIR = Path(__file__).parent / "docs" | |
| ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") | |
| # Initialize services | |
| client = Anthropic(api_key=ANTHROPIC_API_KEY) | |
| doc_intelligence = DocumentIntelligence(DOCS_DIR) | |
| # Supported file extensions | |
| SUPPORTED_EXTENSIONS = {'.md', '.txt', '.rst', '.pdf'} | |
| def load_documentation() -> str: | |
| """Load all documentation files into a single context.""" | |
| all_docs = [] | |
| if not DOCS_DIR.exists(): | |
| return "Documentation directory not found." | |
| for file_path in DOCS_DIR.rglob('*'): | |
| if file_path.is_file() and file_path.suffix in SUPPORTED_EXTENSIONS: | |
| try: | |
| if file_path.suffix == '.pdf': | |
| content = extract_pdf_content(file_path) | |
| else: | |
| content = file_path.read_text(encoding='utf-8', errors='ignore') | |
| relative_path = file_path.relative_to(DOCS_DIR) | |
| all_docs.append(f"=== {relative_path} ===\n{content}\n") | |
| except Exception as e: | |
| all_docs.append(f"=== {file_path.name} ===\nError reading file: {str(e)}\n") | |
| return "\n\n".join(all_docs) if all_docs else "No documentation files found." | |
| def extract_pdf_content(pdf_path: Path) -> str: | |
| """Extract text content from PDF files.""" | |
| try: | |
| from PyPDF2 import PdfReader | |
| reader = PdfReader(pdf_path) | |
| content = [] | |
| for i, page in enumerate(reader.pages, 1): | |
| try: | |
| text = page.extract_text() | |
| content.append(f"--- Page {i} ---\n{text}") | |
| except Exception as e: | |
| content.append(f"--- Page {i} (Error reading: {str(e)}) ---") | |
| return "\n\n".join(content) | |
| except Exception as e: | |
| return f"Error extracting PDF: {str(e)}" | |
| def get_available_files() -> List[str]: | |
| """Get list of available documentation files.""" | |
| files = [] | |
| if DOCS_DIR.exists(): | |
| for file_path in DOCS_DIR.rglob('*'): | |
| if file_path.is_file() and file_path.suffix in SUPPORTED_EXTENSIONS: | |
| files.append(str(file_path.relative_to(DOCS_DIR))) | |
| return sorted(files) | |
| def chat_with_docs(message: str, history: List, system_prompt: str = None) -> str: | |
| """Process user message and generate AI response.""" | |
| if not ANTHROPIC_API_KEY: | |
| return "β οΈ Please set your ANTHROPIC_API_KEY in the .env file." | |
| # Load documentation context | |
| docs_context = load_documentation() | |
| # Build conversation history - support both tuple and dict formats | |
| messages = [] | |
| for item in history: | |
| if isinstance(item, dict): | |
| # Messages format (Gradio 6.x) | |
| if item.get("role") in ["user", "assistant"]: | |
| messages.append({"role": item["role"], "content": item["content"]}) | |
| elif isinstance(item, (list, tuple)) and len(item) == 2: | |
| # Tuple format (Gradio 4.x) | |
| user_msg, assistant_msg = item | |
| if user_msg: | |
| messages.append({"role": "user", "content": user_msg}) | |
| if assistant_msg: | |
| messages.append({"role": "assistant", "content": assistant_msg}) | |
| messages.append({"role": "user", "content": message}) | |
| # Default system prompt | |
| default_system = f"""You are an expert documentation assistant. You have access to the following documentation: | |
| {docs_context} | |
| Your role is to: | |
| - Answer questions accurately based on the documentation | |
| - Provide clear, concise, and helpful responses | |
| - Reference specific sections when relevant | |
| - Admit when information is not in the documentation | |
| - Use markdown formatting for better readability | |
| - Be friendly and professional | |
| Always base your answers on the provided documentation.""" | |
| system = system_prompt if system_prompt else default_system | |
| try: | |
| # Call Claude API with streaming | |
| response_text = "" | |
| with client.messages.stream( | |
| model="claude-3-haiku-20240307", | |
| max_tokens=4096, | |
| system=system, | |
| messages=messages | |
| ) as stream: | |
| for text in stream.text_stream: | |
| response_text += text | |
| return response_text | |
| except Exception as e: | |
| return f"β Error: {str(e)}" | |
| def get_document_stats() -> str: | |
| """Get statistics about the documentation.""" | |
| if not DOCS_DIR.exists(): | |
| return "π No documentation directory found" | |
| files = list(DOCS_DIR.rglob('*')) | |
| doc_files = [f for f in files if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS] | |
| total_size = sum(f.stat().st_size for f in doc_files) / 1024 # KB | |
| stats = f""" | |
| π **Documentation Statistics** | |
| - π Total Files: {len(doc_files)} | |
| - πΎ Total Size: {total_size:.1f} KB | |
| - π Directory: `{DOCS_DIR.name}/` | |
| **File Types:** | |
| """ | |
| by_type = {} | |
| for f in doc_files: | |
| ext = f.suffix | |
| by_type[ext] = by_type.get(ext, 0) + 1 | |
| for ext, count in sorted(by_type.items()): | |
| stats += f"\n- {ext}: {count} files" | |
| return stats | |
| def analyze_document(file_name: str, analysis_type: str) -> str: | |
| """Analyze a specific document.""" | |
| if not file_name: | |
| return "β οΈ Please select a file to analyze" | |
| file_path = DOCS_DIR / file_name | |
| if not file_path.exists(): | |
| return f"β File not found: {file_name}" | |
| try: | |
| if file_path.suffix == '.pdf': | |
| content = extract_pdf_content(file_path) | |
| else: | |
| content = file_path.read_text(encoding='utf-8', errors='ignore') | |
| if analysis_type == "Summary": | |
| summary = doc_intelligence.generate_smart_summary(content, "medium") | |
| return f"π **Summary of {file_name}**\n\n{summary}" | |
| elif analysis_type == "Key Concepts": | |
| concepts = doc_intelligence.extract_key_concepts(content) | |
| result = f"π **Key Concepts in {file_name}**\n\n" | |
| for i, concept in enumerate(concepts[:10], 1): | |
| result += f"{i}. **{concept['concept']}** ({concept['type']}) - appears {concept['frequency']} times\n" | |
| return result | |
| elif analysis_type == "Readability": | |
| analysis = doc_intelligence.analyze_readability(content) | |
| return f""" | |
| π **Readability Analysis of {file_name}** | |
| - **Flesch Reading Ease**: {analysis['flesch_score']} ({analysis['complexity']}) | |
| - **Grade Level**: {analysis['grade_level']} | |
| - **Avg Sentence Length**: {analysis['avg_sentence_length']} words | |
| - **Total Words**: {analysis['total_words']} | |
| - **Total Sentences**: {analysis['total_sentences']} | |
| """ | |
| elif analysis_type == "Q&A Extraction": | |
| qa_pairs = doc_intelligence.extract_questions_and_answers(content) | |
| if not qa_pairs: | |
| return f"β No Q&A pairs found in {file_name}" | |
| result = f"β **Questions & Answers from {file_name}**\n\n" | |
| for i, qa in enumerate(qa_pairs[:5], 1): | |
| result += f"**Q{i}:** {qa['question']}\n**A:** {qa['answer']}\n\n" | |
| return result | |
| except Exception as e: | |
| return f"β Error analyzing file: {str(e)}" | |
| # Custom CSS for sleek indigo/dark mode design with messenger-style chat | |
| custom_css = """ | |
| /* Indigo dark mode color scheme */ | |
| :root { | |
| --bg-primary: #0a0e27; | |
| --bg-secondary: #0f1629; | |
| --bg-tertiary: #1a1f3a; | |
| --accent-primary: #6366f1; | |
| --accent-secondary: #818cf8; | |
| --accent-tertiary: #4f46e5; | |
| --accent-glow: rgba(99, 102, 241, 0.4); | |
| --text-primary: #e0e7ff; | |
| --text-secondary: #a5b4fc; | |
| --border-color: #312e81; | |
| --message-user: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); | |
| --message-bot: #1e1b4b; | |
| --hover-glow: rgba(129, 140, 248, 0.2); | |
| } | |
| /* Main container - full dark mode */ | |
| .gradio-container { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; | |
| background: var(--bg-primary) !important; | |
| } | |
| body, .gradio-container, .main, .contain { | |
| background: var(--bg-primary) !important; | |
| } | |
| /* Header styling - indigo gradient */ | |
| .header-container { | |
| background: linear-gradient(135deg, #4f46e5 0%, #6366f1 50%, #818cf8 100%); | |
| padding: 3rem 2rem; | |
| border-radius: 20px; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 25px 70px rgba(99, 102, 241, 0.5), 0 0 100px rgba(99, 102, 241, 0.2); | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header-container::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: radial-gradient(circle at 30% 50%, rgba(129, 140, 248, 0.3) 0%, transparent 50%); | |
| pointer-events: none; | |
| } | |
| .header-title { | |
| color: white; | |
| font-size: 3rem; | |
| font-weight: 900; | |
| margin: 0; | |
| text-align: center; | |
| text-shadow: 0 0 30px rgba(129, 140, 248, 0.8), 0 4px 20px rgba(0, 0, 0, 0.4); | |
| letter-spacing: -0.02em; | |
| position: relative; | |
| z-index: 1; | |
| background: linear-gradient(to bottom, #ffffff 0%, #e0e7ff 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .header-subtitle { | |
| color: rgba(224, 231, 255, 0.95); | |
| font-size: 1.2rem; | |
| text-align: center; | |
| margin-top: 1rem; | |
| font-weight: 500; | |
| letter-spacing: 0.02em; | |
| position: relative; | |
| z-index: 1; | |
| text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | |
| } | |
| /* Messenger-style chat window with indigo accents */ | |
| .chatbot, [class*="chatbot"] { | |
| background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%) !important; | |
| border: 2px solid var(--border-color) !important; | |
| border-radius: 20px !important; | |
| box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5), 0 0 60px rgba(99, 102, 241, 0.15), inset 0 1px 0 rgba(99, 102, 241, 0.1) !important; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .chatbot::before, [class*="chatbot"]::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); | |
| animation: shimmer 3s infinite; | |
| } | |
| @keyframes shimmer { | |
| 0%, 100% { transform: translateX(-100%); } | |
| 50% { transform: translateX(100%); } | |
| } | |
| /* Chat messages - indigo messenger style with glow */ | |
| .message-wrap { | |
| padding: 0.5rem 1rem !important; | |
| } | |
| .user.message, .message.user { | |
| background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; | |
| color: white !important; | |
| border-radius: 20px 20px 4px 20px !important; | |
| padding: 1rem 1.5rem !important; | |
| margin: 0.75rem 0 !important; | |
| max-width: 75% !important; | |
| margin-left: auto !important; | |
| box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4), 0 0 20px rgba(99, 102, 241, 0.2) !important; | |
| font-size: 0.98rem !important; | |
| line-height: 1.6 !important; | |
| border: 1px solid rgba(99, 102, 241, 0.4) !important; | |
| position: relative; | |
| } | |
| .bot.message, .message.bot { | |
| background: var(--message-bot) !important; | |
| color: var(--text-primary) !important; | |
| border-radius: 20px 20px 20px 4px !important; | |
| padding: 1rem 1.5rem !important; | |
| margin: 0.75rem 0 !important; | |
| max-width: 85% !important; | |
| border: 1px solid var(--border-color) !important; | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(99, 102, 241, 0.1) !important; | |
| font-size: 0.98rem !important; | |
| line-height: 1.7 !important; | |
| backdrop-filter: blur(10px); | |
| } | |
| /* Enhance all message containers */ | |
| .message { | |
| animation: messageSlide 0.3s ease-out; | |
| } | |
| @keyframes messageSlide { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Button styling - indigo gradient with glow */ | |
| button, .primary-button { | |
| background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important; | |
| border: none !important; | |
| border-radius: 14px !important; | |
| font-weight: 700 !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| box-shadow: 0 4px 20px var(--accent-glow), 0 0 20px rgba(99, 102, 241, 0.3) !important; | |
| color: white !important; | |
| padding: 0.875rem 1.75rem !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| font-size: 0.85rem !important; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| button::before, .primary-button::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| button:hover::before, .primary-button:hover::before { | |
| left: 100%; | |
| } | |
| button:hover, .primary-button:hover { | |
| transform: translateY(-3px) scale(1.02) !important; | |
| box-shadow: 0 12px 32px rgba(99, 102, 241, 0.6), 0 0 40px rgba(99, 102, 241, 0.4) !important; | |
| background: linear-gradient(135deg, #818cf8 0%, #6366f1 100%) !important; | |
| } | |
| /* Tabs - indigo dark mode with glow */ | |
| .tabs { | |
| background: var(--bg-secondary) !important; | |
| border-radius: 20px !important; | |
| border: 1px solid var(--border-color) !important; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important; | |
| } | |
| .tab-nav { | |
| background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%) !important; | |
| border-radius: 20px 20px 0 0 !important; | |
| border-bottom: 2px solid var(--border-color) !important; | |
| padding: 0.75rem !important; | |
| box-shadow: inset 0 -1px 0 rgba(99, 102, 241, 0.1) !important; | |
| } | |
| .tab-nav button { | |
| font-weight: 700 !important; | |
| border-radius: 12px !important; | |
| color: var(--text-secondary) !important; | |
| border: 1px solid transparent !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| padding: 0.875rem 1.75rem !important; | |
| margin: 0 0.25rem !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| font-size: 0.85rem !important; | |
| position: relative; | |
| } | |
| .tab-nav button:hover { | |
| background: rgba(99, 102, 241, 0.15) !important; | |
| color: var(--accent-secondary) !important; | |
| border-color: rgba(99, 102, 241, 0.3) !important; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px var(--hover-glow) !important; | |
| } | |
| .tab-nav button.selected { | |
| background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important; | |
| color: white !important; | |
| box-shadow: 0 6px 20px var(--accent-glow), 0 0 30px rgba(99, 102, 241, 0.3) !important; | |
| border-color: rgba(99, 102, 241, 0.5) !important; | |
| transform: translateY(-2px); | |
| } | |
| /* Input fields - indigo dark mode with glow effect */ | |
| textarea, input, .input-box { | |
| background: var(--bg-tertiary) !important; | |
| border: 2px solid var(--border-color) !important; | |
| border-radius: 16px !important; | |
| color: var(--text-primary) !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| padding: 1rem 1.25rem !important; | |
| box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3) !important; | |
| } | |
| textarea:focus, input:focus, .input-box:focus { | |
| border-color: var(--accent-primary) !important; | |
| box-shadow: 0 0 0 4px var(--accent-glow), 0 0 30px rgba(99, 102, 241, 0.3), inset 0 2px 8px rgba(0, 0, 0, 0.2) !important; | |
| background: var(--bg-secondary) !important; | |
| outline: none !important; | |
| transform: translateY(-1px); | |
| } | |
| textarea::placeholder, input::placeholder { | |
| color: var(--text-secondary) !important; | |
| opacity: 0.8 !important; | |
| } | |
| /* Dropdown styling */ | |
| .dropdown, select { | |
| background: var(--bg-tertiary) !important; | |
| border: 2px solid var(--border-color) !important; | |
| border-radius: 12px !important; | |
| color: var(--text-primary) !important; | |
| padding: 0.75rem 1rem !important; | |
| } | |
| /* Stats and info cards - indigo dark mode with depth */ | |
| .stats-box, .info-card { | |
| background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%) !important; | |
| padding: 2rem !important; | |
| border-radius: 20px !important; | |
| border: 1px solid var(--border-color) !important; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(99, 102, 241, 0.1) !important; | |
| color: var(--text-primary) !important; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stats-box::before, .info-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: radial-gradient(circle at 80% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%); | |
| opacity: 0; | |
| transition: opacity 0.4s ease; | |
| pointer-events: none; | |
| } | |
| .info-card:hover, .stats-box:hover { | |
| transform: translateY(-6px) scale(1.01) !important; | |
| box-shadow: 0 16px 48px rgba(99, 102, 241, 0.3), 0 0 60px rgba(99, 102, 241, 0.2) !important; | |
| border-color: var(--accent-primary) !important; | |
| } | |
| .info-card:hover::before, .stats-box:hover::before { | |
| opacity: 1; | |
| } | |
| /* Markdown content styling - indigo dark mode */ | |
| .markdown-text, .prose { | |
| color: var(--text-primary) !important; | |
| line-height: 1.8 !important; | |
| } | |
| .markdown-text h1, .markdown-text h2, .markdown-text h3, | |
| .prose h1, .prose h2, .prose h3 { | |
| color: var(--accent-secondary) !important; | |
| font-weight: 800 !important; | |
| text-shadow: 0 0 20px rgba(99, 102, 241, 0.3); | |
| margin-top: 1.5em !important; | |
| margin-bottom: 0.75em !important; | |
| } | |
| .markdown-text strong, .prose strong { | |
| color: var(--accent-secondary) !important; | |
| font-weight: 700 !important; | |
| } | |
| .markdown-text code, .prose code { | |
| background: var(--bg-tertiary) !important; | |
| color: var(--accent-secondary) !important; | |
| padding: 0.3rem 0.6rem !important; | |
| border-radius: 8px !important; | |
| font-size: 0.9em !important; | |
| border: 1px solid rgba(99, 102, 241, 0.3) !important; | |
| box-shadow: 0 0 10px rgba(99, 102, 241, 0.2) !important; | |
| font-family: 'Fira Code', 'Courier New', monospace !important; | |
| } | |
| .markdown-text pre, .prose pre { | |
| background: linear-gradient(135deg, #0a0e27 0%, #1a1b26 100%) !important; | |
| color: #a9b1d6 !important; | |
| padding: 1.5rem !important; | |
| border-radius: 16px !important; | |
| overflow-x: auto !important; | |
| border: 1px solid var(--border-color) !important; | |
| box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3) !important; | |
| } | |
| .markdown-text a, .prose a { | |
| color: var(--accent-secondary) !important; | |
| text-decoration: none !important; | |
| border-bottom: 2px solid var(--accent-primary) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .markdown-text a:hover, .prose a:hover { | |
| color: var(--accent-primary) !important; | |
| text-shadow: 0 0 10px rgba(99, 102, 241, 0.5) !important; | |
| } | |
| /* Labels and text */ | |
| label, .label { | |
| color: var(--text-primary) !important; | |
| font-weight: 600 !important; | |
| font-size: 0.95rem !important; | |
| margin-bottom: 0.5rem !important; | |
| } | |
| .secondary-text { | |
| color: var(--text-secondary) !important; | |
| } | |
| /* Remove white backgrounds globally */ | |
| .block, .form, .panel { | |
| background: var(--bg-secondary) !important; | |
| border: 1px solid var(--border-color) !important; | |
| border-radius: 12px !important; | |
| } | |
| /* Scrollbar styling - indigo themed */ | |
| ::-webkit-scrollbar { | |
| width: 12px; | |
| height: 12px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-secondary); | |
| border-radius: 10px; | |
| border: 1px solid var(--border-color); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: linear-gradient(180deg, var(--accent-primary) 0%, var(--accent-tertiary) 100%); | |
| border-radius: 10px; | |
| border: 2px solid var(--bg-secondary); | |
| box-shadow: 0 0 10px rgba(99, 102, 241, 0.3); | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: linear-gradient(180deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); | |
| box-shadow: 0 0 20px rgba(99, 102, 241, 0.5); | |
| } | |
| /* Loading animation */ | |
| .pending { | |
| background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-color) 50%, var(--bg-tertiary) 75%); | |
| background-size: 200% 100%; | |
| animation: loading 1.5s ease-in-out infinite; | |
| } | |
| @keyframes loading { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| /* Examples section */ | |
| .examples { | |
| background: var(--bg-tertiary) !important; | |
| border-radius: 12px !important; | |
| border: 1px solid var(--border-color) !important; | |
| padding: 1rem !important; | |
| } | |
| .example-item { | |
| background: var(--bg-secondary) !important; | |
| border: 1px solid var(--border-color) !important; | |
| border-radius: 8px !important; | |
| color: var(--text-primary) !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .example-item:hover { | |
| background: var(--bg-tertiary) !important; | |
| border-color: var(--accent-primary) !important; | |
| transform: translateX(4px) !important; | |
| } | |
| /* Radio buttons */ | |
| .radio-group { | |
| background: var(--bg-tertiary) !important; | |
| border-radius: 12px !important; | |
| padding: 0.5rem !important; | |
| } | |
| """ | |
| # Create the Gradio interface with dark theme - backward compatible | |
| try: | |
| # Try Gradio 4.x+ with css parameter | |
| demo = gr.Blocks( | |
| css=custom_css, | |
| title="Docs Navigator MCP - AI Documentation Assistant" | |
| ) | |
| except TypeError: | |
| # Fallback for older Gradio versions | |
| demo = gr.Blocks(title="Docs Navigator MCP - AI Documentation Assistant") | |
| # CSS will be injected via HTML if needed | |
| with demo: | |
| # Inject CSS and Header with indigo gradient | |
| gr.HTML(f""" | |
| <style> | |
| {custom_css} | |
| </style> | |
| <div class="header-container"> | |
| <h1 class="header-title">π Docs Navigator MCP</h1> | |
| <p class="header-subtitle">β¨ AI-Powered Documentation Intelligence with Claude β¨</p> | |
| </div> | |
| """) | |
| with gr.Tabs() as tabs: | |
| # Chat Tab - Messenger Style | |
| with gr.Tab("π¬ Chat", id=0): | |
| gr.HTML(""" | |
| <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'> | |
| <strong style='color: var(--accent-secondary);'>π¬ AI Chat Assistant</strong><br> | |
| Ask anything about your documentation - I have full context of all your files. | |
| </div> | |
| """) | |
| try: | |
| chatbot = gr.Chatbot( | |
| height=550, | |
| show_label=False, | |
| type="messages" | |
| ) | |
| except TypeError: | |
| # Fallback for older Gradio versions | |
| chatbot = gr.Chatbot( | |
| height=550, | |
| show_label=False | |
| ) | |
| with gr.Row(): | |
| msg = gr.Textbox( | |
| placeholder="π Message the docs assistant...", | |
| show_label=False, | |
| scale=5, | |
| container=False, | |
| lines=1, | |
| max_lines=3 | |
| ) | |
| submit_btn = gr.Button("Send", scale=1, variant="primary", elem_classes="primary-button", size="lg") | |
| with gr.Row(): | |
| clear_btn = gr.Button("ποΈ Clear", size="sm", variant="secondary") | |
| gr.HTML("<div style='flex-grow: 1;'></div>") | |
| with gr.Accordion("π‘ Example Questions", open=False): | |
| gr.Examples( | |
| examples=[ | |
| "What is this documentation about?", | |
| "How do I get started with setup?", | |
| "What are the main features?", | |
| "Show me troubleshooting steps", | |
| "What configuration options are available?", | |
| "Explain the architecture" | |
| ], | |
| inputs=msg, | |
| label=None | |
| ) | |
| # Document Analysis Tab | |
| with gr.Tab("π Analysis", id=1): | |
| gr.HTML(""" | |
| <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'> | |
| <strong style='color: var(--accent-secondary);'>π Document Intelligence</strong><br> | |
| Deep analysis of individual documentation files with AI-powered insights. | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| file_dropdown = gr.Dropdown( | |
| choices=get_available_files(), | |
| label="π Select Document", | |
| interactive=True, | |
| container=True | |
| ) | |
| analysis_type = gr.Radio( | |
| choices=["Summary", "Key Concepts", "Readability", "Q&A Extraction"], | |
| value="Summary", | |
| label="π― Analysis Type", | |
| container=True | |
| ) | |
| analyze_btn = gr.Button("π Analyze Document", variant="primary", elem_classes="primary-button", size="lg") | |
| refresh_btn = gr.Button("π Refresh Files", size="sm", variant="secondary") | |
| with gr.Column(scale=2): | |
| analysis_output = gr.Markdown( | |
| value="<div style='text-align: center; padding: 3rem; color: #8b949e;'><div style='font-size: 2.5rem; margin-bottom: 1rem;'>π</div><div style='font-size: 1.1rem;'>Select a document and analysis type to begin</div></div>", | |
| elem_classes="info-card" | |
| ) | |
| # Statistics Tab | |
| with gr.Tab("π Stats", id=2): | |
| gr.HTML(""" | |
| <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'> | |
| <strong style='color: var(--accent-secondary);'>π Documentation Overview</strong><br> | |
| Real-time statistics and insights about your documentation collection. | |
| </div> | |
| """) | |
| stats_display = gr.Markdown( | |
| value=get_document_stats(), | |
| elem_classes="stats-box" | |
| ) | |
| refresh_stats_btn = gr.Button("π Refresh Statistics", variant="secondary", size="sm") | |
| # Settings Tab | |
| with gr.Tab("βοΈ Settings", id=3): | |
| gr.HTML(""" | |
| <div style='padding: 1rem 0 0.5rem 0; color: var(--text-secondary); font-size: 0.95rem;'> | |
| <strong style='color: var(--accent-secondary);'>βοΈ Configuration</strong><br> | |
| Customize the AI assistant behavior and view system information. | |
| </div> | |
| """) | |
| with gr.Group(): | |
| custom_system_prompt = gr.Textbox( | |
| label="π¨ Custom System Prompt (Optional)", | |
| placeholder="Enter a custom system prompt to modify the AI's personality and behavior...", | |
| lines=6, | |
| info="π‘ Leave empty to use the default documentation assistant prompt", | |
| container=True | |
| ) | |
| gr.HTML(""" | |
| <div style='margin-top: 2rem; padding: 2rem; background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%); border-radius: 16px; border: 1px solid var(--border-color);'> | |
| <h3 style='color: var(--accent-secondary); margin: 0 0 1rem 0; font-size: 1.5rem;'>π About Docs Navigator MCP</h3> | |
| <p style='color: var(--text-primary); line-height: 1.8; margin-bottom: 1.5rem;'> | |
| An intelligent documentation assistant combining <strong>Claude AI</strong> with | |
| <strong>Model Context Protocol (MCP)</strong> for powerful document analysis and Q&A. | |
| </p> | |
| <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1.5rem;'> | |
| <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'> | |
| <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>π€</div> | |
| <div style='color: var(--text-primary); font-weight: 600;'>Claude 3 Haiku</div> | |
| <div style='color: var(--text-secondary); font-size: 0.85rem;'>Latest AI Model</div> | |
| </div> | |
| <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'> | |
| <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>π</div> | |
| <div style='color: var(--text-primary); font-weight: 600;'>Smart Analysis</div> | |
| <div style='color: var(--text-secondary); font-size: 0.85rem;'>Document Intelligence</div> | |
| </div> | |
| <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'> | |
| <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>π</div> | |
| <div style='color: var(--text-primary); font-weight: 600;'>Multi-Format</div> | |
| <div style='color: var(--text-secondary); font-size: 0.85rem;'>MD, TXT, RST, PDF</div> | |
| </div> | |
| <div style='padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);'> | |
| <div style='font-size: 1.5rem; margin-bottom: 0.5rem;'>β‘</div> | |
| <div style='color: var(--text-primary); font-weight: 600;'>Fast & Responsive</div> | |
| <div style='color: var(--text-secondary); font-size: 0.85rem;'>Streaming Responses</div> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| gr.HTML("<div style='margin: 2rem 0 1rem 0; padding-top: 1.5rem; border-top: 1px solid var(--border-color);'></div>") | |
| api_status = gr.Markdown( | |
| value=f""" | |
| ### π API Status | |
| {"β **Anthropic API Key Configured** - Ready to use!" if ANTHROPIC_API_KEY else "β οΈ **API Key Missing** - Set `ANTHROPIC_API_KEY` in your `.env` file"} | |
| """, | |
| elem_classes="info-card" | |
| ) | |
| # Event handlers | |
| def respond(message, chat_history, system_prompt): | |
| if not message.strip(): | |
| return "", chat_history | |
| response = chat_with_docs(message, chat_history, system_prompt if system_prompt.strip() else None) | |
| # Detect format and append accordingly | |
| if chat_history and isinstance(chat_history[0], dict): | |
| # Messages format | |
| chat_history.append({"role": "user", "content": message}) | |
| chat_history.append({"role": "assistant", "content": response}) | |
| else: | |
| # Tuple format | |
| chat_history.append((message, response)) | |
| return "", chat_history | |
| msg.submit(respond, [msg, chatbot, custom_system_prompt], [msg, chatbot]) | |
| submit_btn.click(respond, [msg, chatbot, custom_system_prompt], [msg, chatbot]) | |
| clear_btn.click(lambda: [], None, chatbot) | |
| analyze_btn.click( | |
| analyze_document, | |
| [file_dropdown, analysis_type], | |
| analysis_output | |
| ) | |
| refresh_btn.click( | |
| lambda: gr.update(choices=get_available_files()), | |
| None, | |
| file_dropdown | |
| ) | |
| refresh_stats_btn.click( | |
| get_document_stats, | |
| None, | |
| stats_display | |
| ) | |
| if __name__ == "__main__": | |
| print("π Starting Docs Navigator MCP...") | |
| print("π AI-Powered Documentation Assistant") | |
| print("π‘ Ask questions about your documentation!") | |
| print("-" * 50) | |
| # Detect if running on Hugging Face Spaces | |
| is_spaces = os.getenv("SPACE_ID") is not None | |
| demo.launch( | |
| server_name="0.0.0.0" if is_spaces else "127.0.0.1", | |
| server_port=7860, | |
| show_error=True, | |
| share=False | |
| ) |