import dash from dash import dcc, html, Input, Output, State import dash_bootstrap_components as dbc from dash.exceptions import PreventUpdate import google.generativeai as genai from github import Github import gitlab import requests import tempfile import docx import os import logging import threading from huggingface_hub import HfApi from flask import send_file global docx_path, md_path docx_path = None md_path = None # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Initialize Dash app app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) server = app.server # Expose the Flask server # Hugging Face API setup hf_api = HfApi() # Get Hugging Face variables GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY') def is_ui_file(filename): ui_extensions = ['.erb', '.haml', '.slim', '.php', '.aspx', '.jsp', '.ftl', '.twig', '.mustache', '.handlebars', '.ejs', '.pug', '.blade.php', '.xhtml', '.fxml', '.tsx', '.jsx', '.vue', '.html', '.cshtml', '.razor', '.xaml', '.jsx'] return any(filename.endswith(ext) for ext in ui_extensions) def get_file_contents(git_provider, repo_url, exclude_folders): file_contents = [] logger.info(f"Fetching files from {git_provider} repository: {repo_url}") if exclude_folders is None: exclude_folders = [] else: exclude_folders = [folder.strip() for folder in exclude_folders.split(',') if folder.strip()] if git_provider == "GitHub": g = Github(GITHUB_TOKEN) repo = g.get_repo(repo_url) contents = repo.get_contents("") while contents: file_content = contents.pop(0) if file_content.type == "dir": if not any(file_content.path.startswith(folder) for folder in exclude_folders): contents.extend(repo.get_contents(file_content.path)) elif is_ui_file(file_content.name) and not any(file_content.path.startswith(folder) for folder in exclude_folders): logger.info(f"Found UI file: {file_content.path}") file_contents.append((file_content.path, file_content.decoded_content.decode('utf-8', errors='ignore'))) elif git_provider == "GitLab": gl = gitlab.Gitlab(url='https://gitlab.com', private_token=GITHUB_TOKEN) project = gl.projects.get(repo_url) items = project.repository_tree(recursive=True) for item in items: if item['type'] == 'blob' and is_ui_file(item['name']) and not any(item['path'].startswith(folder) for folder in exclude_folders): logger.info(f"Found UI file: {item['path']}") file_content = project.files.get(item['path'], ref='main') file_contents.append((item['path'], file_content.decode().decode('utf-8', errors='ignore'))) elif git_provider == "Gitea": base_url = "https://gitea.com/api/v1" headers = {"Authorization": f"token {GITHUB_TOKEN}"} def recursive_get_contents(path=""): response = requests.get(f"{base_url}/repos/{repo_url}/contents/{path}", headers=headers) response.raise_for_status() for item in response.json(): if item['type'] == 'file' and is_ui_file(item['name']) and not any(item['path'].startswith(folder) for folder in exclude_folders): logger.info(f"Found UI file: {item['path']}") file_content = requests.get(item['download_url']).text file_contents.append((item['path'], file_content)) elif item['type'] == 'dir' and not any(item['path'].startswith(folder) for folder in exclude_folders): recursive_get_contents(item['path']) recursive_get_contents() else: raise ValueError("Unsupported Git provider") logger.info(f"Total UI files found: {len(file_contents)}") return file_contents def generate_guide_section(file_path, file_content, guide_type): logger.info(f"Generating {guide_type} section for file: {file_path}") genai.configure(api_key=GEMINI_API_KEY) model = genai.GenerativeModel('gemini-2.0-flash-lite') if guide_type == "User Guide": prompt = f"""Based on the following UI-related code file, generate a section for a user guide: File: {file_path} Content: {file_content} Please focus on: 1. The specific features and functionality this UI component provides to the end users 2. Step-by-step instructions on how to use these features 3. Any user interactions or inputs required 4. Expected outcomes or results for the user Important formatting instructions: - The output should be in plain text no markdown for example do not use * or ** or # or ##. Instead use numbers like 1., 2. for bullets - Use clear section titles - Follow this numbering heirarchy (1.0, 1.1, 1.2), (2.0, 2.1, 2.2), (3.0, 3.1, 3.2) - Explain the purpose and benefit of each feature for non-technical users - This is an end user manual, not a system administration manual so focus on the end user components """ else: # Administration Guide prompt = f"""Based on the following UI-related code file, generate a section for an System guide: File: {file_path} Content: {file_content} Please focus on explaining what that component is and does: 1. Any configuration options or settings related to this UI component 2. Security considerations or access control related to this feature 3. How to monitor or troubleshoot issues with this component 4. Best practices for managing and maintaining this part of the system Important formatting instructions: - The output should be in plain text no markdown for example for example do not use * or ** or # or ##. Instead use numbers like 1., 2. for bullets - Use clear section titles - Use clear section titles that has the name of the file in parenthesis - Follow this numbering heirarchy (1.0, 1.1, 1.2), (2.0, 2.1, 2.2), (3.0, 3.1, 3.2) - Explain the purpose and implications of each component """ response = model.generate_content(prompt) logger.info(f"Generated {guide_type} section for {file_path}") return response.text def generate_guide(git_provider, repo_url, guide_type, exclude_folders): try: logger.info(f"Starting guide generation for {repo_url}") file_contents = get_file_contents(git_provider, repo_url, exclude_folders) guide_sections = [] for file_path, content in file_contents: section = generate_guide_section(file_path, content, guide_type) guide_sections.append(section) logger.info(f"Added section for {file_path}") full_guide = f"# {guide_type}\n\n" + "\n\n".join(guide_sections) logger.info("Creating DOCX file") doc = docx.Document() doc.add_heading(guide_type, 0) for line in full_guide.split('\n'): line = line.strip() if line.startswith('# '): doc.add_heading(line[2:], level=1) elif line.startswith('## '): doc.add_heading(line[3:], level=2) elif line.startswith('Step'): doc.add_paragraph(line, style='List Number') else: doc.add_paragraph(line) with tempfile.NamedTemporaryFile(delete=False, suffix='.docx') as temp_docx: doc.save(temp_docx.name) docx_path = temp_docx.name logger.info(f"DOCX file saved: {docx_path}") logger.info("Creating Markdown file") with tempfile.NamedTemporaryFile(delete=False, suffix='.md', mode='w', encoding='utf-8') as temp_md: temp_md.write(full_guide) md_path = temp_md.name logger.info(f"Markdown file saved: {md_path}") logger.info("Guide generation completed successfully") return full_guide, docx_path, md_path except Exception as e: logger.error(f"An error occurred: {str(e)}", exc_info=True) return f"An error occurred: {str(e)}", None, None # App layout app.layout = dbc.Container([ dbc.Row([ dbc.Col([ html.H1("Automated Guide Generator", className="text-center my-4"), html.P("Generate a user guide or administration guide based on the UI-related code in a Git repository using Gemini AI. Select a Git provider, enter repository details, choose the guide type, and let AI create a comprehensive guide.", className="text-center mb-4"), dbc.Card([ dbc.CardBody([ dbc.Form([ dbc.Select( id="git-provider", options=[ {"label": "GitHub", "value": "GitHub"}, {"label": "GitLab", "value": "GitLab"}, {"label": "Gitea", "value": "Gitea"} ], value="GitHub", # Set default value to GitHub className="mb-3", ), dbc.Input(id="repo-url", type="text", placeholder="Repository URL (owner/repo)", className="mb-3"), dbc.RadioItems( id="guide-type", options=[ {"label": "User Guide", "value": "User Guide"}, {"label": "Administration Guide", "value": "Administration Guide"} ], value="User Guide", # Set default value to User Guide inline=True, className="mb-3", ), dbc.Input(id="exclude-folders", type="text", placeholder="Exclude Folders (comma-separated)", className="mb-3"), dbc.Button("Generate Guide", id="generate-button", color="primary", className="mt-3"), ]) ]) ], className="mb-4"), dbc.Spinner( dbc.Card([ dbc.CardBody([ html.H4("Generated Guide", className="card-title"), html.Div([ dbc.Button("Download DOCX", id="download-docx", color="secondary", className="me-2"), dbc.Button("Download Markdown", id="download-md", color="secondary"), ], className="mt-3"), dcc.Download(id="download-docx-file"), dcc.Download(id="download-md-file"), ]) ], className="mt-4"), color="primary", ), ], width=12), # Changed to full width ]) ], fluid=True) # Update the callback @app.callback( [Output("download-docx", "n_clicks"), Output("download-md", "n_clicks")], [Input("generate-button", "n_clicks")], [State("git-provider", "value"), State("repo-url", "value"), State("guide-type", "value"), State('exclude-folders', 'value')] ) def update_output(n_clicks, git_provider, repo_url, guide_type, exclude_folders): if n_clicks is None: raise PreventUpdate global docx_path, md_path def generate_guide_thread(): global docx_path, md_path guide_text, docx_path, md_path = generate_guide(git_provider, repo_url, guide_type, exclude_folders) return guide_text thread = threading.Thread(target=generate_guide_thread) thread.start() thread.join() # Wait for the thread to complete return 0, 0 # Reset n_clicks for download buttons @app.callback( Output("download-docx-file", "data"), Input("download-docx", "n_clicks"), prevent_initial_call=True, ) def download_docx(n_clicks): if n_clicks is None or docx_path is None: raise PreventUpdate return dcc.send_file(docx_path, filename="generated_guide.docx") @app.callback( Output("download-md-file", "data"), Input("download-md", "n_clicks"), prevent_initial_call=True, ) def download_md(n_clicks): if n_clicks is None or md_path is None: raise PreventUpdate return dcc.send_file(md_path, filename="generated_guide.md") if __name__ == '__main__': print("Starting the Dash application...") app.run(debug=False, host='0.0.0.0', port=7860) print("Dash application has finished running.")