Spaces:
Sleeping
Sleeping
| import os | |
| import uuid | |
| from fastapi import FastAPI, UploadFile, File, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.templating import Jinja2Templates | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from dotenv import load_dotenv | |
| from langgraph.graph import StateGraph | |
| from playwright_model import generate_ui_report | |
| from agents.graph_builder_langgraph import graph # Import the compiled agent pipeline | |
| # ========================================================== | |
| # 🔧 Setup | |
| # ========================================================== | |
| load_dotenv("settings.env") | |
| app = FastAPI(title="PowerApps Mockup Generator") | |
| # Enable CORS (allow all for testing, restrict in production) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Paths | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") | |
| OUTPUT_FOLDER = os.path.join(BASE_DIR, "static", "outputs") | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| os.makedirs(OUTPUT_FOLDER, exist_ok=True) | |
| # Static + templates | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| templates = Jinja2Templates(directory="templates") | |
| # ========================================================== | |
| # 🏠 Root Route | |
| # ========================================================== | |
| async def index(request: Request): | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| # ========================================================== | |
| # 📄 File Upload → Agent Pipeline (UI Generation) | |
| # ========================================================== | |
| import traceback | |
| async def upload_files(request: Request, files: list[UploadFile] = File(...)): | |
| try: | |
| file_paths = [] | |
| for file in files: | |
| filename = f"{uuid.uuid4()}_{file.filename}" | |
| filepath = os.path.join(UPLOAD_FOLDER, filename) | |
| with open(filepath, "wb") as f: | |
| f.write(await file.read()) | |
| file_paths.append(filepath) | |
| print("🚀 Running document → requirements → UI generation pipeline...") | |
| result = await graph.ainvoke({"files": file_paths}) | |
| # Convert to dict if needed | |
| if hasattr(result, "dict"): | |
| result = result.dict() | |
| generated_html = result.get("html") | |
| public_url = result.get("public_url") | |
| print("✅ Final graph result:", result) | |
| if not generated_html: | |
| return JSONResponse({"error": "UI generation failed — empty HTML returned."}, status_code=500) | |
| return JSONResponse({"html": generated_html, "link": public_url}) | |
| except Exception as e: | |
| print("❌ Full pipeline error trace:") | |
| traceback.print_exc() | |
| return JSONResponse({"error": f"AI Pipeline failed: {str(e)}"}, status_code=500) | |
| # ========================================================== | |
| # 📘 Generate PDF Report (Triggered by Button) | |
| # ========================================================== | |
| async def generate_report(request: Request): | |
| """Generate UI walkthrough report using Playwright after UI is visible.""" | |
| data = await request.json() | |
| app_url = data.get("url") | |
| if not app_url: | |
| return JSONResponse({"error": "No URL provided"}, status_code=400) | |
| output_pdf = os.path.join(OUTPUT_FOLDER, "UI_Report.pdf") | |
| try: | |
| await generate_ui_report(app_url, output_pdf) | |
| return FileResponse(output_pdf, filename="UI_Report.pdf", media_type="application/pdf") | |
| except Exception as e: | |
| return JSONResponse({"error": f"PDF generation failed: {str(e)}"}, status_code=500) | |
| import os | |
| import uuid | |
| import re | |
| from fastapi import FastAPI, UploadFile, File, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.templating import Jinja2Templates | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from dotenv import load_dotenv | |
| from pdfminer.high_level import extract_text as pdf_extract_text | |
| from docx import Document | |
| import google.generativeai as genai | |
| import fpdf | |
| # ========================================================== | |
| # 🔧 Setup | |
| # ========================================================== | |
| load_dotenv("settings.env") | |
| app = FastAPI(title="PowerApps Mockup Generator") | |
| # Enable CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # ⚠️ Restrict in production | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Directory setup | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") | |
| OUTPUT_FOLDER = os.path.join(BASE_DIR, "static", "outputs") | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| os.makedirs(OUTPUT_FOLDER, exist_ok=True) | |
| # Serve static files | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| # Setup templates | |
| templates = Jinja2Templates(directory="templates") | |
| # Gemini setup | |
| genai.configure(api_key=os.environ.get("GEMINI_API_KEY2")) | |
| model = genai.GenerativeModel("gemini-2.5-pro") | |
| # ========================================================== | |
| # 📄 Helper Function | |
| # ========================================================== | |
| def extract_text_from_file(filepath: str) -> str: | |
| """Extract text from a PDF or DOCX file.""" | |
| if filepath.endswith(".pdf"): | |
| return pdf_extract_text(filepath) | |
| elif filepath.endswith(".docx"): | |
| doc = Document(filepath) | |
| return "\n".join([p.text for p in doc.paragraphs]) | |
| return "" | |
| # ========================================================== | |
| # 🧭 Routes | |
| # ========================================================== | |
| async def index(request: Request): | |
| """Serve the main UI.""" | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| async def upload_files(request: Request, files: list[UploadFile] = File(...)): | |
| """Handle uploaded files and generate PowerApps-style HTML.""" | |
| all_text = "" | |
| for file in files: | |
| filename = f"{uuid.uuid4()}_{file.filename}" | |
| filepath = os.path.join(UPLOAD_FOLDER, filename) | |
| with open(filepath, "wb") as f: | |
| f.write(await file.read()) | |
| extracted_text = extract_text_from_file(filepath) | |
| all_text += f"\n\nDocument: {file.filename}\n{extracted_text}" | |
| if not all_text.strip(): | |
| return JSONResponse({"error": "No readable content found in files."}, status_code=400) | |
| reference_path = os.path.join(BASE_DIR, "templates", "demo_qms_design.html") | |
| if not os.path.exists(reference_path): | |
| return JSONResponse({"error": "Reference design not found."}, status_code=500) | |
| with open(reference_path, "r", encoding="utf-8") as f: | |
| reference_html = f.read() | |
| # ========================================================== | |
| # 🧠 Gemini Prompt | |
| # ========================================================== | |
| prompt = f""" | |
| You are an expert PowerApps-style UI generator and business workflow designer. | |
| Your goal is to create a Recruitment Management Application HTML layout that follows **exactly the same layout, structure, and JavaScript logic** as the reference QMS app design provided below. | |
| ### 🔧 INSTRUCTIONS | |
| 1️⃣ **Mimic the UI structure and components** of the QMS reference exactly: | |
| - Keep the **sidebar**, **header**, and **multi-screen logic** identical. | |
| - Each .menu-item must open a corresponding .screen using the same showScreen() JS pattern. | |
| - Each workflow stage (like Draft → Manage → Review → etc.) should be clickable and update the workflow progress using the same logic from the reference. | |
| 2️⃣ **Adapt the content** to Recruitment Workflow: | |
| Replace QMS-specific names with Recruitment equivalents while keeping UI consistency. For example: | |
| - “QMS Request” → “Recruitment Request” | |
| - “Draft / Review / Approval / Published” → “Requisition / Sourcing / Interview / Offer / Hired” | |
| - “Document Type / Identifier” → “Job Role / Candidate ID” | |
| - “Reviewer / Approver” → “Hiring Manager / HR Lead” | |
| 3️⃣ **Keep CSS and JS identical** to the reference except where labels or icons differ. | |
| - Preserve all existing class names (e.g., .workflow-step, .screen, .menu-item, .kanban-board). | |
| - Only modify inner HTML and labels. | |
| - Keep inline scripts functional — do not remove event handlers. | |
| 4️⃣ **Functional Requirements** | |
| - Must be a single standalone HTML document (no markdown, no external JS). | |
| - Include embedded <style> and <script> tags (like the reference). | |
| - Maintain all transitions, dynamic toggles, and progress updates. | |
| - All UI parts (menu, header, cards, workflow) must remain interactive. | |
| - Icons should use Font Awesome 6 syntax. | |
| 5️⃣ **Sidebar and Navigation Consistency (Dynamic Rule)** | |
| - Automatically detect all sidebar sections and menu items from the reference design’s `<div class="sidebar">` block. | |
| - Recreate the **same sidebar structure, menu hierarchy, and icons** exactly as they appear in the reference. | |
| - Preserve every `<div class="menu-title">` and `<div class="menu-item">` from the reference, reusing their labels and `<i class="fas ...">` icons. | |
| - Only replace text labels where the business context clearly requires recruitment-related equivalents (e.g., “QMS Request” → “Requisition”), but never remove or omit any original menu entries. | |
| - Maintain the same `onclick="showScreen('...')"` mapping and `active` class behavior, ensuring that the **default active screen and sidebar highlight** remain consistent with the reference layout. | |
| - If new screens are generated from recruitment documents, append them under the most relevant section while preserving the reference menu’s ordering and visual hierarchy. | |
| 📘 Reference Design: | |
| -------------------- | |
| {reference_html} | |
| -------------------- | |
| 📄 Recruitment Document Content: | |
| -------------------- | |
| {all_text} | |
| -------------------- | |
| """ | |
| # ========================================================== | |
| # 🤖 Gemini Call | |
| # ========================================================== | |
| try: | |
| response = model.generate_content(prompt) | |
| generated_html = response.text.strip() | |
| except Exception as e: | |
| return JSONResponse({"error": f"Gemini API error: {str(e)}"}, status_code=500) | |
| # Clean up possible markdown fences | |
| if generated_html.startswith("```"): | |
| generated_html = generated_html.split("```html")[-1] | |
| generated_html = generated_html.split("```")[-1].strip() | |
| matches = re.findall(r"<html.*?</html>", generated_html, re.DOTALL | re.IGNORECASE) | |
| if matches: | |
| generated_html = matches[0] | |
| # ========================================================== | |
| # 💾 Save output | |
| # ========================================================== | |
| output_filename = f"{uuid.uuid4()}.html" | |
| output_path = os.path.join(OUTPUT_FOLDER, output_filename) | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| f.write(generated_html) | |
| # ========================================================== | |
| # 🌐 Auto-detect base URL | |
| # ========================================================== | |
| host = request.url.hostname | |
| scheme = request.url.scheme | |
| base_url = f"{scheme}://{host}" | |
| public_url = f"{base_url}/static/outputs/{output_filename}" | |
| # ========================================================== | |
| # ✅ Return result | |
| # ========================================================== | |
| return JSONResponse({ | |
| "html": generated_html, | |
| "link": public_url | |
| }) | |
| from fastapi.responses import FileResponse | |
| from playwright_model import generate_ui_report | |
| async def generate_report(request: Request): | |
| """Generate a UI walkthrough report PDF using Playwright.""" | |
| data = await request.json() | |
| app_url = data.get("url") | |
| if not app_url: | |
| return JSONResponse({"error": "No URL provided"}, status_code=400) | |
| output_pdf = os.path.join(OUTPUT_FOLDER, "UI_Report.pdf") | |
| await generate_ui_report(app_url, output_pdf) | |
| return FileResponse(output_pdf, filename="UI_Report.pdf", media_type="application/pdf") |