|
import streamlit as st |
|
import sqlite3 |
|
import hashlib |
|
import os |
|
import google.generativeai as genai |
|
import zipfile |
|
from git import Repo |
|
from transformers import AutoModelForCausalLM, AutoTokenizer |
|
import torch |
|
import requests |
|
from fpdf import FPDF |
|
import markdown |
|
import tempfile |
|
|
|
|
|
|
|
DB_FILE = "users.db" |
|
|
|
def create_user_table(): |
|
conn = sqlite3.connect(DB_FILE) |
|
cursor = conn.cursor() |
|
cursor.execute(""" |
|
CREATE TABLE IF NOT EXISTS users ( |
|
username TEXT PRIMARY KEY, |
|
password TEXT |
|
) |
|
""") |
|
conn.commit() |
|
conn.close() |
|
|
|
def add_user(username, password): |
|
conn = sqlite3.connect(DB_FILE) |
|
cursor = conn.cursor() |
|
hashed_password = hashlib.sha256(password.encode()).hexdigest() |
|
try: |
|
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, hashed_password)) |
|
conn.commit() |
|
except sqlite3.IntegrityError: |
|
st.error("Username already exists. Please choose a different username.") |
|
conn.close() |
|
|
|
def authenticate_user(username, password): |
|
conn = sqlite3.connect(DB_FILE) |
|
cursor = conn.cursor() |
|
hashed_password = hashlib.sha256(password.encode()).hexdigest() |
|
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, hashed_password)) |
|
user = cursor.fetchone() |
|
conn.close() |
|
return user |
|
|
|
def initialize_session_state(): |
|
if "authenticated" not in st.session_state: |
|
st.session_state.authenticated = False |
|
if "username" not in st.session_state: |
|
st.session_state.username = None |
|
if "page" not in st.session_state: |
|
st.session_state.page = "login" |
|
if "current_project" not in st.session_state: |
|
st.session_state.current_project = None |
|
if "project_uploaded" not in st.session_state: |
|
st.session_state.project_uploaded = False |
|
|
|
def main(): |
|
st.title("SimplifAI") |
|
|
|
|
|
initialize_session_state() |
|
|
|
|
|
create_user_table() |
|
|
|
|
|
if st.session_state.page == "login": |
|
login_page() |
|
elif st.session_state.page == "project_staging": |
|
project_staging_page() |
|
elif st.session_state.page == "project_view": |
|
project_view_page() |
|
elif st.session_state.page == "generate_documentation": |
|
generate_documentation_page() |
|
elif st.session_state.page == "saved_documentation": |
|
saved_documentation_page() |
|
|
|
def login_page(): |
|
st.image("SimplifAI Logo.jpeg", use_container_width=True) |
|
|
|
st.subheader("Please Log In or Register to Continue") |
|
auth_mode = st.radio("Choose an Option", ["Log In", "Register"], horizontal=True) |
|
|
|
if auth_mode == "Log In": |
|
st.subheader("Log In") |
|
username = st.text_input("Username", key="login_username") |
|
password = st.text_input("Password", type="password", key="login_password") |
|
|
|
|
|
if st.button("Log In"): |
|
if authenticate_user(username, password): |
|
st.session_state.authenticated = True |
|
st.session_state.username = username |
|
st.session_state.page = "project_staging" |
|
else: |
|
st.error("Invalid username or password. Please try again.") |
|
|
|
elif auth_mode == "Register": |
|
st.subheader("Register") |
|
username = st.text_input("Create Username", key="register_username") |
|
password = st.text_input("Create Password", type="password", key="register_password") |
|
|
|
|
|
if st.button("Register"): |
|
if username and password: |
|
add_user(username, password) |
|
st.success("Account created successfully! You can now log in.") |
|
else: |
|
st.error("Please fill in all fields.") |
|
|
|
|
|
def project_staging_page(): |
|
|
|
st.sidebar.title(f"Hello, {st.session_state.username}!") |
|
if st.sidebar.button("Log Out"): |
|
st.session_state.authenticated = False |
|
st.session_state.username = None |
|
st.session_state.page = "login" |
|
|
|
|
|
user_folder = os.path.join("user_projects", st.session_state.username) |
|
os.makedirs(user_folder, exist_ok=True) |
|
|
|
|
|
projects = [d for d in os.listdir(user_folder) if os.path.isdir(os.path.join(user_folder, d))] |
|
|
|
|
|
selected_project = st.sidebar.selectbox("Projects", ["Select a project"] + projects) |
|
|
|
if selected_project != "Select a project": |
|
st.session_state.current_project = selected_project |
|
st.session_state.page = "project_view" |
|
st.rerun() |
|
|
|
|
|
if st.session_state.project_uploaded: |
|
st.success(f"Project '{st.session_state.current_project}' uploaded successfully!") |
|
st.session_state.project_uploaded = False |
|
|
|
|
|
st.subheader("Project Staging") |
|
st.write("You can create a new project by uploading files or folders, or by cloning a GitHub repository.") |
|
|
|
|
|
|
|
|
|
action = st.radio("Choose an action", ["Upload Files or Folders", "Clone GitHub Repository"], horizontal=True) |
|
|
|
project_name = st.text_input("Enter a project name") |
|
|
|
if action == "Upload Files or Folders": |
|
st.subheader("Upload Files or Folders") |
|
uploaded_files = st.file_uploader( |
|
"Upload one or more files or a .zip archive for folders", accept_multiple_files=True |
|
) |
|
|
|
if uploaded_files and project_name: |
|
if st.button("Upload Project"): |
|
project_folder = os.path.join(user_folder, project_name) |
|
os.makedirs(project_folder, exist_ok=True) |
|
|
|
for uploaded_file in uploaded_files: |
|
|
|
file_path = os.path.join(project_folder, uploaded_file.name) |
|
|
|
with open(file_path, "wb") as f: |
|
f.write(uploaded_file.getbuffer()) |
|
|
|
|
|
if uploaded_file.name.endswith(".zip"): |
|
try: |
|
with zipfile.ZipFile(file_path, "r") as zip_ref: |
|
zip_ref.extractall(project_folder) |
|
os.remove(file_path) |
|
st.success(f"Folder from {uploaded_file.name} extracted successfully!") |
|
except zipfile.BadZipFile: |
|
st.error(f"File {uploaded_file.name} is not a valid .zip file.") |
|
else: |
|
st.success(f"File {uploaded_file.name} saved successfully!") |
|
|
|
|
|
st.session_state.current_project = project_name |
|
st.session_state.project_uploaded = True |
|
st.rerun() |
|
|
|
elif action == "Clone GitHub Repository": |
|
st.subheader("Clone GitHub Repository") |
|
repo_url = st.text_input("Enter the GitHub repository URL") |
|
|
|
if repo_url and project_name: |
|
if st.button("Upload Project"): |
|
project_folder = os.path.join(user_folder, project_name) |
|
os.makedirs(project_folder, exist_ok=True) |
|
|
|
try: |
|
Repo.clone_from(repo_url, project_folder) |
|
|
|
st.session_state.current_project = project_name |
|
st.session_state.project_uploaded = True |
|
st.rerun() |
|
except Exception as e: |
|
st.error(f"Failed to clone repository: {e}") |
|
|
|
|
|
|
|
|
|
|
|
gemini = os.getenv("GEMINI") |
|
genai.configure(api_key=gemini) |
|
model = genai.GenerativeModel("gemini-1.5-flash") |
|
|
|
|
|
|
|
def read_project_files(project_path): |
|
"""Reads all files in the project directory and its subdirectories.""" |
|
file_paths = [] |
|
for root, _, files in os.walk(project_path): |
|
for file in files: |
|
|
|
if ".git" not in root: |
|
file_paths.append(os.path.join(root, file)) |
|
return file_paths |
|
|
|
def read_files(file_paths): |
|
"""Reads content from a list of file paths.""" |
|
file_contents = {} |
|
for file_path in file_paths: |
|
if os.path.exists(file_path): |
|
try: |
|
|
|
with open(file_path, 'r', encoding='utf-8') as file: |
|
file_contents[file_path] = file.read() |
|
except UnicodeDecodeError: |
|
print(f"Skipping binary or non-UTF-8 file: {file_path}") |
|
else: |
|
print(f"File not found: {file_path}") |
|
return file_contents |
|
|
|
|
|
def generate_prompt(file_contents, functionality_description): |
|
"""Generates a prompt for Gemini to analyze the files.""" |
|
prompt = "Analyze the following code files to identify all functions required to implement the functionality: " |
|
prompt += f"'{functionality_description}'.\n\n" |
|
|
|
for file_path, content in file_contents.items(): |
|
prompt += f"File: {os.path.basename(file_path)}\n{content}\n\n" |
|
|
|
prompt += "For each relevant function, provide:\n" |
|
prompt += "1. Which file the function is found in.\n" |
|
prompt += "2. The function name.\n" |
|
prompt += "3. Dependencies on other functions or modules.\n" |
|
|
|
prompt += """ |
|
Return your output in the following format providing no commentary: |
|
Project Summary: |
|
<A summary of the project> |
|
Functionality: |
|
<a more precise version of the user defined funcionality> |
|
Functions: |
|
<for each file containing relevant functions> |
|
<file name of the file that contains relevant functions>: |
|
-<relevant function header from the current file>: |
|
-Function Dependencies:<function dependencies seperated by commas> |
|
""" |
|
|
|
return prompt |
|
|
|
def identify_required_functions(project_path, functionality_description): |
|
"""Identifies required functions for a specified functionality.""" |
|
|
|
file_paths = read_project_files(project_path) |
|
|
|
|
|
file_contents = read_files(file_paths) |
|
|
|
|
|
prompt = generate_prompt(file_contents, functionality_description) |
|
|
|
|
|
response = model.generate_content(prompt) |
|
|
|
|
|
return response.text |
|
|
|
|
|
def extract_cleaned_gemini_output(gemini_output): |
|
""" |
|
Removes the 'Functions' section and any irrelevant content from the Gemini output. |
|
Args: |
|
gemini_output (str): The raw output returned by Gemini. |
|
Returns: |
|
str: Cleaned output without the 'Functions' section. |
|
""" |
|
lines = gemini_output.splitlines() |
|
cleaned_output = [] |
|
skip_section = False |
|
|
|
for line in lines: |
|
line = line.strip() |
|
|
|
|
|
if line.startswith("Functions:"): |
|
skip_section = True |
|
|
|
|
|
if skip_section and (line.startswith("Tasks:") or line.startswith("Project Summary:")): |
|
skip_section = False |
|
|
|
|
|
if not skip_section: |
|
cleaned_output.append(line) |
|
|
|
|
|
return "\n".join(line for line in cleaned_output if line) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def split_into_chunks(content, chunk_size=1000): |
|
"""Splits large content into smaller chunks.""" |
|
return [content[i:i + chunk_size] for i in range(0, len(content), chunk_size)] |
|
|
|
def generate_detailed_documentation(file_contents, functionality_description): |
|
""" |
|
Generates detailed documentation using Gemini directly. |
|
Args: |
|
file_contents (dict): A dictionary with file paths as keys and their content as values. |
|
functionality_description (str): A description of the functionality to document. |
|
Returns: |
|
str: The generated documentation. |
|
""" |
|
prompt = f""" |
|
The following code files are provided. Analyze their contents and generate comprehensive documentation. |
|
Functionality description: '{functionality_description}' |
|
Tasks: |
|
1. Generate a project summary: |
|
' |
|
Project Summary: |
|
<Include project description and library or module dependencies> |
|
' |
|
2. Refine the user-defined functionality: |
|
' |
|
Functionality Summary: |
|
<Provide an enhanced description of user-specified functionality> |
|
' |
|
3. Describe the functionality flow: |
|
' |
|
Functionality Flow: |
|
<In point form, explain the sequence of functions and data flow mentioning each function being used. If there are multiple initial input points required for the user-defined functionality, show the flow of each one |
|
and all each function being used. Make sure to have NO empty line between 'Functionality Flow' and this content> |
|
' |
|
4. Generate detailed documentation for each function in the codebase: |
|
' |
|
Function Documentation: |
|
For each function: |
|
- '<function name>': |
|
- Summary: <Description of the function's purpose> |
|
- Inputs: <Details of inputs and their types> |
|
- Outputs: <Details of outputs and their types> |
|
- Dependencies: <Dependencies on other modules/functions> |
|
- Data structures: <Details of data structures used> |
|
- Algorithmic Details: <Description of the algorithm used> |
|
- Error Handling: <Description of how the function handles errors> |
|
- Assumptions: <Any assumptions the function makes> |
|
- Example Usage: <Example demonstrating usage> |
|
' |
|
Please return only the required documentation in the specified format. |
|
Code files: |
|
""" |
|
for file_path, content in file_contents.items(): |
|
prompt += f"\nFile: {os.path.basename(file_path)}\n{content}\n" |
|
|
|
response = model.generate_content(prompt) |
|
return process_gemini_output(response.text.strip()) |
|
|
|
def process_gemini_output(output): |
|
""" |
|
Processes the raw output from Gemini to format it appropriately. |
|
|
|
- Removes ``` at the top and bottom of the output. |
|
- Removes all pairs of **. |
|
- Replaces ` used as a quote with '. |
|
- Replaces leading '*' with '-' for lists. |
|
|
|
Args: |
|
output (str): The raw Gemini output. |
|
|
|
Returns: |
|
str: The processed and cleaned output. |
|
""" |
|
|
|
if output.startswith("```") and output.endswith("```"): |
|
output = output[3:-3].strip() |
|
|
|
|
|
output = output.replace("**", "") |
|
|
|
|
|
output = output.replace("`", "'") |
|
|
|
|
|
lines = output.splitlines() |
|
processed_lines = [] |
|
for line in lines: |
|
stripped_line = line.strip() |
|
if stripped_line.startswith("*"): |
|
|
|
processed_lines.append(line.replace("*", "-", 1)) |
|
else: |
|
processed_lines.append(line) |
|
|
|
return "\n".join(processed_lines) |
|
|
|
def generate_documentation_page(): |
|
|
|
st.sidebar.title(f"Project: {st.session_state.current_project}") |
|
if st.sidebar.button("Back to Project"): |
|
st.session_state.page = "project_view" |
|
st.rerun() |
|
if st.sidebar.button("Log Out"): |
|
st.session_state.authenticated = False |
|
st.session_state.username = None |
|
st.session_state.page = "login" |
|
st.rerun() |
|
|
|
st.subheader(f"Generate Documentation for {st.session_state.current_project}") |
|
st.write("Enter the functionality or parts of the project for which you'd like to generate documentation.") |
|
|
|
|
|
functionality = st.text_area( |
|
"Describe the functionality", |
|
placeholder="e.g., Explain the function of the file `main.py`", |
|
) |
|
|
|
|
|
if st.button("Analyze"): |
|
if functionality.strip(): |
|
st.write("Analyzing project files... Please wait.") |
|
|
|
|
|
user_folder = os.path.join("user_projects", st.session_state.username) |
|
project_folder = os.path.join(user_folder, st.session_state.current_project) |
|
|
|
if os.path.exists(project_folder): |
|
try: |
|
|
|
file_paths = read_project_files(project_folder) |
|
|
|
|
|
file_contents = read_files(file_paths) |
|
|
|
|
|
documentation = generate_detailed_documentation(file_contents, functionality) |
|
|
|
|
|
st.session_state.generated_documentation = documentation |
|
|
|
|
|
st.success("Documentation generated successfully!") |
|
st.text_area("Generated Documentation", documentation, height=600) |
|
except Exception as e: |
|
st.error(f"An error occurred: {e}") |
|
else: |
|
st.error("Project folder not found. Ensure the GitHub repository was cloned successfully.") |
|
else: |
|
st.error("Please enter the functionality to analyze.") |
|
|
|
|
|
if "generated_documentation" in st.session_state and st.session_state.generated_documentation: |
|
documentation = st.session_state.generated_documentation |
|
|
|
|
|
user_folder = os.path.join("user_projects", st.session_state.username) |
|
os.makedirs(user_folder, exist_ok=True) |
|
|
|
pdf_path = os.path.join(user_folder, f"{st.session_state.current_project}_Documentation.pdf") |
|
markdown_path = os.path.join(user_folder, f"{st.session_state.current_project}_Documentation.md") |
|
|
|
|
|
pdf_generated = os.path.exists(pdf_path) |
|
markdown_generated = os.path.exists(markdown_path) |
|
|
|
|
|
if not pdf_generated: |
|
if st.button("Generate PDF"): |
|
try: |
|
generate_pdf(documentation, pdf_path) |
|
st.session_state.pdf_generated = True |
|
st.rerun() |
|
except Exception as e: |
|
st.error(f"Failed to generate PDF: {e}") |
|
else: |
|
try: |
|
with open(pdf_path, "rb") as pdf_file: |
|
st.download_button( |
|
label="Download PDF", |
|
data=pdf_file.read(), |
|
file_name=f"{st.session_state.current_project}_Documentation.pdf", |
|
mime="application/pdf", |
|
) |
|
except FileNotFoundError: |
|
st.write("Click 'Generate PDF' to be able to download and store the documentation as a PDF") |
|
|
|
if not markdown_generated: |
|
if st.button("Generate Markdown File"): |
|
try: |
|
generate_markdown_file(documentation, markdown_path) |
|
st.session_state.markdown_generated = True |
|
st.rerun() |
|
except Exception as e: |
|
st.error(f"Failed to generate Markdown file: {e}") |
|
else: |
|
try: |
|
with open(markdown_path, "rb") as markdown_file: |
|
st.download_button( |
|
label="Download Markdown File", |
|
data=markdown_file.read(), |
|
file_name=f"{st.session_state.current_project}_Documentation.md", |
|
mime="text/markdown", |
|
) |
|
except FileNotFoundError: |
|
st.write("Click 'Generate Markdown File' to be able to download and store the documentation as a markdown file") |
|
|
|
|
|
|
|
def generate_pdf(documentation, pdf_path): |
|
pdf = FPDF() |
|
pdf.set_auto_page_break(auto=True, margin=15) |
|
pdf.add_page() |
|
pdf.set_font("Courier", size=12) |
|
|
|
|
|
for line in documentation.splitlines(): |
|
try: |
|
if line.startswith("- '"): |
|
pdf.set_font("Courier", style="B", size=12) |
|
pdf.multi_cell(0, 10, line.encode('latin-1', 'replace').decode('latin-1')) |
|
elif line.startswith("Project Summary:") or line.startswith("Functionality Summary:") or \ |
|
line.startswith("Functionality Flow:") or line.startswith("Function Documentation:"): |
|
pdf.set_font("Courier", style="B", size=14) |
|
pdf.multi_cell(0, 10, line.encode('latin-1', 'replace').decode('latin-1')) |
|
else: |
|
pdf.set_font("Courier", size=12) |
|
pdf.multi_cell(0, 10, line.encode('latin-1', 'replace').decode('latin-1')) |
|
except UnicodeEncodeError as e: |
|
pdf.multi_cell(0, 10, line.encode('latin-1', 'replace').decode('latin-1')) |
|
|
|
|
|
pdf.output(pdf_path) |
|
|
|
|
|
|
|
|
|
|
|
def generate_markdown_file(documentation, output_path): |
|
""" |
|
Generates a markdown file from the documentation with structured formatting. |
|
Args: |
|
documentation (str): The documentation content to format and save. |
|
output_path (str): Path to save the generated markdown file. |
|
""" |
|
formatted_lines = [] |
|
for line in documentation.splitlines(): |
|
|
|
if line.startswith("- '") and line.endswith("':"): |
|
formatted_lines.append(f"**{line.strip()}**") |
|
|
|
elif line.startswith("Project Summary:") or line.startswith("Functionality Summary:") or \ |
|
line.startswith("Functionality Flow:") or line.startswith("Function Documentation:"): |
|
formatted_lines.append(f"# {line.strip()}") |
|
else: |
|
|
|
formatted_lines.append(line) |
|
|
|
|
|
with open(output_path, "w") as f: |
|
f.write("\n".join(formatted_lines)) |
|
|
|
|
|
|
|
|
|
def saved_documentation_page(): |
|
|
|
st.sidebar.title(f"Project: {st.session_state.current_project}") |
|
if st.sidebar.button("Back to Project"): |
|
st.session_state.page = "project_view" |
|
st.rerun() |
|
if st.sidebar.button("Log Out"): |
|
st.session_state.authenticated = False |
|
st.session_state.username = None |
|
st.session_state.page = "login" |
|
st.rerun() |
|
|
|
st.subheader(f"Saved Documentation for {st.session_state.current_project}") |
|
st.write("Below is a list of saved documentation for this project. Click to download.") |
|
|
|
|
|
user_folder = os.path.join("user_projects", st.session_state.username) |
|
pdf_path = os.path.join(user_folder, f"{st.session_state.current_project}_Documentation.pdf") |
|
markdown_path = os.path.join(user_folder, f"{st.session_state.current_project}_Documentation.md") |
|
|
|
|
|
pdf_exists = os.path.exists(pdf_path) |
|
markdown_exists = os.path.exists(markdown_path) |
|
|
|
|
|
if not pdf_exists and not markdown_exists: |
|
st.info("No documentation has been generated yet. Please go to the 'Generate Documentation' page.") |
|
else: |
|
if pdf_exists: |
|
with open(pdf_path, "rb") as pdf_file: |
|
st.download_button( |
|
label="Download PDF Documentation", |
|
data=pdf_file.read(), |
|
file_name=f"{st.session_state.current_project}_Documentation.pdf", |
|
mime="application/pdf", |
|
) |
|
|
|
if markdown_exists: |
|
with open(markdown_path, "rb") as markdown_file: |
|
st.download_button( |
|
label="Download Markdown Documentation", |
|
data=markdown_file.read(), |
|
file_name=f"{st.session_state.current_project}_Documentation.md", |
|
mime="text/markdown", |
|
) |
|
|
|
|
|
|
|
def project_view_page(): |
|
|
|
st.sidebar.title(f"Project: {st.session_state.current_project}") |
|
if st.sidebar.button("Back to Project Staging"): |
|
st.session_state.page = "project_staging" |
|
st.rerun() |
|
if st.sidebar.button("Log Out"): |
|
st.session_state.authenticated = False |
|
st.session_state.username = None |
|
st.session_state.page = "login" |
|
st.rerun() |
|
|
|
|
|
st.subheader(f"Project: {st.session_state.current_project}") |
|
st.write("Manage your project and explore its files.") |
|
|
|
|
|
if st.button("Generate Documentation"): |
|
st.session_state.page = "generate_documentation" |
|
st.rerun() |
|
|
|
if st.button("Saved Documentation"): |
|
st.session_state.page = "saved_documentation" |
|
st.rerun() |
|
|
|
|
|
if "show_file_structure" not in st.session_state: |
|
st.session_state.show_file_structure = False |
|
|
|
if st.button("Show File Structure"): |
|
st.session_state.show_file_structure = not st.session_state.show_file_structure |
|
|
|
if st.session_state.show_file_structure: |
|
user_folder = os.path.join("user_projects", st.session_state.username) |
|
project_folder = os.path.join(user_folder, st.session_state.current_project) |
|
|
|
st.write("File structure:") |
|
|
|
for root, dirs, files in os.walk(project_folder): |
|
level = root.replace(project_folder, "").count(os.sep) |
|
indent = " " * 4 * level |
|
|
|
if level == 0: |
|
st.write(f"π {os.path.basename(root)}") |
|
else: |
|
with st.expander(f"{indent}π {os.path.basename(root)}"): |
|
sub_indent = " " * 4 * (level + 1) |
|
for file in files: |
|
st.write(f"{sub_indent}π {file}") |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|
|
|