mockup_agent / app.py
dina1's picture
Update app.py
9f2b1af verified
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
# ==========================================================
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# ==========================================================
# 📄 File Upload → Agent Pipeline (UI Generation)
# ==========================================================
import traceback
@app.post("/upload")
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)
# ==========================================================
@app.post("/generate_report")
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
# ==========================================================
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
"""Serve the main UI."""
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/upload")
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
@app.post("/generate_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")