|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import google.generativeai as genai |
|
|
import gradio as gr |
|
|
from html2image import Html2Image |
|
|
import pdfplumber |
|
|
import uuid |
|
|
import os |
|
|
import re |
|
|
import time |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
genai.configure(api_key=os.environ.get("GEMINI_API_KEY")) |
|
|
model = genai.GenerativeModel("gemini-2.5-pro") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hti = Html2Image(browser_executable="/usr/bin/chromium") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BASE_TEMPLATE = """ |
|
|
<html> |
|
|
<head> |
|
|
<style> |
|
|
:root { |
|
|
--primary-blue: #0078d4; |
|
|
--primary-dark: #005a9e; |
|
|
--sidebar-bg: #2d2d2d; |
|
|
--sidebar-hover: #3a3a3a; |
|
|
--sidebar-active: #0078d4; |
|
|
--card-bg: #ffffff; |
|
|
--card-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
|
--border-radius: 4px; |
|
|
--text-dark: #323130; |
|
|
--text-light: #605e5c; |
|
|
--success: #107c10; |
|
|
--warning: #d83b01; |
|
|
--background: #f5f5f5; |
|
|
} |
|
|
|
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', 'Segoe UI Web (West European)', sans-serif; |
|
|
background: var(--background); |
|
|
color: var(--text-dark); |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.app-container { |
|
|
display: flex; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
/* Sidebar Styles */ |
|
|
.sidebar { |
|
|
width: 240px; |
|
|
background: var(--sidebar-bg); |
|
|
color: white; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.sidebar-header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 16px; |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
background: #1f1f1f; |
|
|
border-bottom: 1px solid #444; |
|
|
} |
|
|
|
|
|
.sidebar-header svg { |
|
|
margin-right: 12px; |
|
|
} |
|
|
|
|
|
.sidebar-nav { |
|
|
flex: 1; |
|
|
padding: 8px 0; |
|
|
} |
|
|
|
|
|
.sidebar-section { |
|
|
padding: 8px 0; |
|
|
border-bottom: 1px solid #444; |
|
|
} |
|
|
|
|
|
.section-label { |
|
|
padding: 8px 16px; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
color: #aaa; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
|
|
|
.sidebar-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 10px 16px; |
|
|
color: #ccc; |
|
|
text-decoration: none; |
|
|
transition: all 0.2s ease; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.sidebar-item:hover { |
|
|
background: var(--sidebar-hover); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.sidebar-item.active { |
|
|
background: var(--sidebar-active); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.sidebar-item svg { |
|
|
margin-right: 12px; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
} |
|
|
|
|
|
/* Main Content Styles */ |
|
|
.main-content { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.topbar { |
|
|
height: 52px; |
|
|
background: white; |
|
|
border-bottom: 1px solid #e1e1e1; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
padding: 0 20px; |
|
|
} |
|
|
|
|
|
.topbar-left { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 16px; |
|
|
} |
|
|
|
|
|
.topbar-title { |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
color: var(--text-dark); |
|
|
} |
|
|
|
|
|
.topbar-right { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.topbar-icon { |
|
|
width: 32px; |
|
|
height: 32px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
transition: background 0.2s ease; |
|
|
} |
|
|
|
|
|
.topbar-icon:hover { |
|
|
background: #f0f0f0; |
|
|
} |
|
|
|
|
|
.user-info { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
padding: 4px 8px; |
|
|
border-radius: 4px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.user-avatar { |
|
|
width: 28px; |
|
|
height: 28px; |
|
|
border-radius: 50%; |
|
|
background: var(--primary-blue); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
/* Content Area */ |
|
|
.content-area { |
|
|
flex: 1; |
|
|
padding: 20px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
/* PowerApps Card Components */ |
|
|
.card { |
|
|
background: var(--card-bg); |
|
|
border-radius: var(--border-radius); |
|
|
box-shadow: var(--card-shadow); |
|
|
margin-bottom: 16px; |
|
|
border: 1px solid #e1e1e1; |
|
|
} |
|
|
|
|
|
.card-header { |
|
|
padding: 16px 20px; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
display: flex; |
|
|
justify-content: between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.card-title { |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
color: var(--text-dark); |
|
|
} |
|
|
|
|
|
.card-actions { |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.card-body { |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.card-footer { |
|
|
padding: 12px 20px; |
|
|
border-top: 1px solid #f0f0f0; |
|
|
background: #fafafa; |
|
|
} |
|
|
|
|
|
/* Grid System */ |
|
|
.grid { |
|
|
display: grid; |
|
|
gap: 16px; |
|
|
} |
|
|
|
|
|
.grid-2 { |
|
|
grid-template-columns: 1fr 1fr; |
|
|
} |
|
|
|
|
|
.grid-3 { |
|
|
grid-template-columns: 1fr 1fr 1fr; |
|
|
} |
|
|
|
|
|
.grid-4 { |
|
|
grid-template-columns: 1fr 1fr 1fr 1fr; |
|
|
} |
|
|
|
|
|
/* Buttons */ |
|
|
.btn { |
|
|
padding: 8px 16px; |
|
|
border: none; |
|
|
border-radius: var(--border-radius); |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: var(--primary-blue); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover { |
|
|
background: var(--primary-dark); |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: #f0f0f0; |
|
|
color: var(--text-dark); |
|
|
border: 1px solid #d1d1d1; |
|
|
} |
|
|
|
|
|
.btn-secondary:hover { |
|
|
background: #e5e5e5; |
|
|
} |
|
|
|
|
|
.btn-success { |
|
|
background: var(--success); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-warning { |
|
|
background: var(--warning); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
/* Tables */ |
|
|
.table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
} |
|
|
|
|
|
.table th, |
|
|
.table td { |
|
|
padding: 12px 16px; |
|
|
text-align: left; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
} |
|
|
|
|
|
.table th { |
|
|
background: #fafafa; |
|
|
font-weight: 600; |
|
|
color: var(--text-dark); |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.table td { |
|
|
color: var(--text-light); |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.table tr:hover { |
|
|
background: #f8f8f8; |
|
|
} |
|
|
|
|
|
/* Form Elements */ |
|
|
.form-group { |
|
|
margin-bottom: 16px; |
|
|
} |
|
|
|
|
|
.form-label { |
|
|
display: block; |
|
|
margin-bottom: 6px; |
|
|
font-weight: 600; |
|
|
color: var(--text-dark); |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.form-control { |
|
|
width: 100%; |
|
|
padding: 8px 12px; |
|
|
border: 1px solid #d1d1d1; |
|
|
border-radius: var(--border-radius); |
|
|
font-size: 14px; |
|
|
transition: border 0.2s ease; |
|
|
} |
|
|
|
|
|
.form-control:focus { |
|
|
outline: none; |
|
|
border-color: var(--primary-blue); |
|
|
box-shadow: 0 0 0 1px var(--primary-blue); |
|
|
} |
|
|
|
|
|
/* Status Indicators */ |
|
|
.status-badge { |
|
|
padding: 4px 8px; |
|
|
border-radius: 12px; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.status-success { |
|
|
background: #dff6dd; |
|
|
color: var(--success); |
|
|
} |
|
|
|
|
|
.status-warning { |
|
|
background: #fff4ce; |
|
|
color: #8a6500; |
|
|
} |
|
|
|
|
|
.status-error { |
|
|
background: #fde7e9; |
|
|
color: #a80000; |
|
|
} |
|
|
|
|
|
.status-info { |
|
|
background: #deecf9; |
|
|
color: var(--primary-blue); |
|
|
} |
|
|
|
|
|
/* Quick Action Tiles */ |
|
|
.tile-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|
|
gap: 16px; |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.tile { |
|
|
background: white; |
|
|
border-radius: var(--border-radius); |
|
|
padding: 20px; |
|
|
text-align: center; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
border: 1px solid #e1e1e1; |
|
|
} |
|
|
|
|
|
.tile:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15); |
|
|
} |
|
|
|
|
|
.tile-icon { |
|
|
width: 48px; |
|
|
height: 48px; |
|
|
margin: 0 auto 12px; |
|
|
background: var(--primary-blue); |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.tile-title { |
|
|
font-weight: 600; |
|
|
margin-bottom: 4px; |
|
|
color: var(--text-dark); |
|
|
} |
|
|
|
|
|
.tile-description { |
|
|
font-size: 12px; |
|
|
color: var(--text-light); |
|
|
} |
|
|
|
|
|
/* Activity Feed */ |
|
|
.activity-item { |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
padding: 12px 0; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
} |
|
|
|
|
|
.activity-item:last-child { |
|
|
border-bottom: none; |
|
|
} |
|
|
|
|
|
.activity-dot { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
border-radius: 50%; |
|
|
background: var(--primary-blue); |
|
|
margin-right: 12px; |
|
|
margin-top: 6px; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.activity-content { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.activity-text { |
|
|
margin-bottom: 4px; |
|
|
color: var(--text-dark); |
|
|
} |
|
|
|
|
|
.activity-time { |
|
|
font-size: 12px; |
|
|
color: var(--text-light); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="app-container"> |
|
|
{user_sidebar} |
|
|
<div class="main-content"> |
|
|
{user_topbar} |
|
|
<div class="content-area"> |
|
|
{user_content} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SVG_FLUENT = { |
|
|
"hamburger": """<svg viewBox="0 0 24 24"><path fill="#fff" d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z"/></svg>""", |
|
|
"home": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>""", |
|
|
"recent": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 8v5l4 2 .7-1.2-3.2-1.8V8h-1.5zM12 2a10 10 0 00-10 10H0l4 4 4-4H5a7 7 0 117 7 7 7 0 01-7-7H3a9 9 0 109-9z"/></svg>""", |
|
|
"pinned": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2v2l2 2v3l2 2v2H6v-2l2-2V6l2-2V2h4zm-1 13v7h-2v-7h2z"/></svg>""", |
|
|
"business": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>""", |
|
|
"archive": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0 4h2v-2H3v2zm0 4h18V5H3v16zM19 7v10H5V7h14z"/></svg>""", |
|
|
"workflow": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M17 12h-5v5h5v-5zm-5-2h5V5h-5v5zm2-3h1v1h-1V7zm0 5h1v1h-1v-1zm-8 5h5v-5H6v5zm0-7h5V5H6v5zm2-3h1v1H8V7zm0 5h1v1H8v-1zM3 3h18v18H3V3zm16 16V5H5v14h14z"/></svg>""", |
|
|
"search": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>""", |
|
|
"settings": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>""", |
|
|
"users": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A2.01 2.01 0 0018.06 7h-2.12c-.93 0-1.76.55-2.13 1.33l-.19.46-3.53 1.29c-.45.17-.75.61-.75 1.1v7.82h2V14h3v8h3zm-7.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S11 9.17 11 10s.67 1.5 1.5 1.5zM5.5 6c1.11 0 2-.89 2-2s-.89-2-2-2-2 .89-2 2 .89 2 2 2zm2 16v-7H5v7h2.5zm2-16c1.11 0 2-.89 2-2s-.89-2-2-2-2 .89-2 2 .89 2 2 2zM13 22v-7h-2v7h2z"/></svg>""", |
|
|
"templates": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/></svg>""", |
|
|
"add": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>""", |
|
|
"chevron_down": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>""", |
|
|
"menu": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3zM3 12h18v2H3zM3 18h18v2H3z"/></svg>""", |
|
|
"back": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>""", |
|
|
"share": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.5 2.5 0 000-1.4l7.02-4.11A2.5 2.5 0 0018 7.91a2.5 2.5 0 10-2.5-2.5 2.5 2.5 0 00-.1.71L8.59 10.3a2.5 2.5 0 100 3.4l7.02 4.11a2.5 2.5 0 00-.1.71 2.5 2.5 0 102.5-2.44z"/></svg>""", |
|
|
"check": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17 4.83 12l-1.42 1.41L9 19l12-12-1.41-1.41z"/></svg>""", |
|
|
"info": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 9h2V7h-2v2zm1-7C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6z"/></svg>""", |
|
|
"filter": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M10 18h4v-2h-4v2zm-7-7v2h18v-2H3zm3-5v2h12V6H6z"/></svg>""", |
|
|
"user_icon": """<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.67 0 8 1.34 8 4v2H4v-2c0-2.66 5.33-4 8-4zm0-2a4 4 0 110-8 4 4 0 010 8z"/></svg>""" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_sidebar(app_title, screens, active_label="Dashboard"): |
|
|
"""Generate PowerApps-style sidebar with proper sections and icons""" |
|
|
|
|
|
sidebar_html = f""" |
|
|
<div class="sidebar"> |
|
|
<div class="sidebar-header"> |
|
|
{SVG_FLUENT['menu']} |
|
|
<span>{app_title}</span> |
|
|
</div> |
|
|
<div class="sidebar-nav"> |
|
|
""" |
|
|
|
|
|
|
|
|
sidebar_html += '<div class="sidebar-section">' |
|
|
sidebar_html += '<div class="section-label">Navigation</div>' |
|
|
|
|
|
main_nav = [ |
|
|
("home", "Home"), |
|
|
("recent", "Recent"), |
|
|
("pinned", "Pinned"), |
|
|
("business", "Businesses") |
|
|
] |
|
|
|
|
|
for icon_key, label in main_nav: |
|
|
active_class = "active" if label.lower() == active_label.lower() else "" |
|
|
sidebar_html += f""" |
|
|
<div class="sidebar-item {active_class}"> |
|
|
{SVG_FLUENT[icon_key]} |
|
|
<span>{label}</span> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
sidebar_html += '</div>' |
|
|
|
|
|
|
|
|
sidebar_html += '<div class="sidebar-section">' |
|
|
sidebar_html += '<div class="section-label">My Work</div>' |
|
|
|
|
|
for i, screen in enumerate(screens): |
|
|
screen_name = screen.get("screen_name", f"Screen {i+1}") |
|
|
screen_label = re.sub(r'screen$', '', screen_name, flags=re.IGNORECASE).strip() |
|
|
active_class = "active" if screen_name == active_label else "" |
|
|
|
|
|
|
|
|
if "dashboard" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['home'] |
|
|
elif "search" in screen_name.lower() or "archive" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['search'] |
|
|
elif "workflow" in screen_name.lower() or "submission" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['workflow'] |
|
|
elif "user" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['users'] |
|
|
elif "template" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['templates'] |
|
|
elif "setting" in screen_name.lower(): |
|
|
icon = SVG_FLUENT['settings'] |
|
|
else: |
|
|
icon = SVG_FLUENT['business'] |
|
|
|
|
|
sidebar_html += f""" |
|
|
<div class="sidebar-item {active_class}"> |
|
|
{icon} |
|
|
<span>{screen_label}</span> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
sidebar_html += '</div>' |
|
|
|
|
|
|
|
|
sidebar_html += '<div class="sidebar-section">' |
|
|
sidebar_html += '<div class="section-label">Metadata</div>' |
|
|
|
|
|
metadata_items = [ |
|
|
("Document Type", "templates"), |
|
|
("Division", "business"), |
|
|
("Process Area", "workflow"), |
|
|
("Topic", "archive") |
|
|
] |
|
|
|
|
|
for label, icon_key in metadata_items: |
|
|
sidebar_html += f""" |
|
|
<div class="sidebar-item"> |
|
|
{SVG_FLUENT[icon_key]} |
|
|
<span>{label}</span> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
sidebar_html += '</div>' |
|
|
|
|
|
sidebar_html += """ |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return sidebar_html |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_topbar(screen_name, role="default", user_name="John Joe"): |
|
|
"""Generate dynamic PowerApps-style top bar with role-based icons""" |
|
|
|
|
|
initials = "".join([x[0] for x in user_name.split()[:2]]).upper() |
|
|
|
|
|
|
|
|
if role == "dashboard": |
|
|
left_icon = SVG_FLUENT['menu'] |
|
|
right_icons = f""" |
|
|
{SVG_FLUENT['share']} |
|
|
{SVG_FLUENT['info']} |
|
|
""" |
|
|
elif role == "list": |
|
|
left_icon = SVG_FLUENT['menu'] |
|
|
right_icons = f""" |
|
|
{SVG_FLUENT['add']} |
|
|
{SVG_FLUENT['filter']} |
|
|
""" |
|
|
elif role == "form": |
|
|
left_icon = SVG_FLUENT['back'] |
|
|
right_icons = f""" |
|
|
{SVG_FLUENT['check']} |
|
|
""" |
|
|
elif role == "settings": |
|
|
left_icon = SVG_FLUENT['back'] |
|
|
right_icons = f""" |
|
|
{SVG_FLUENT['info']} |
|
|
""" |
|
|
else: |
|
|
left_icon = SVG_FLUENT['menu'] |
|
|
right_icons = "" |
|
|
|
|
|
return f""" |
|
|
<div class="topbar"> |
|
|
<div class="topbar-left"> |
|
|
<div class="topbar-icon">{left_icon}</div> |
|
|
<div class="topbar-title">{screen_name}</div> |
|
|
</div> |
|
|
<div class="topbar-right"> |
|
|
{right_icons} |
|
|
<div class="user-info"> |
|
|
<span>Welcome, {user_name}</span> |
|
|
<div class="user-avatar">{initials}</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_dashboard_content(): |
|
|
"""Generate PowerApps-style dashboard content""" |
|
|
return """ |
|
|
<div class="tile-grid"> |
|
|
<div class="tile"> |
|
|
<div class="tile-icon">📁</div> |
|
|
<div class="tile-title">Start New Archival</div> |
|
|
<div class="tile-description">Upload a document and initiate the archival workflow</div> |
|
|
</div> |
|
|
<div class="tile"> |
|
|
<div class="tile-icon">📊</div> |
|
|
<div class="tile-title">My Submissions</div> |
|
|
<div class="tile-description">Track the status of your submitted documents</div> |
|
|
</div> |
|
|
<div class="tile"> |
|
|
<div class="tile-icon">🔍</div> |
|
|
<div class="tile-title">Search Archive</div> |
|
|
<div class="tile-description">Find and view officially archived QMS documents</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-2"> |
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">Recent Activity</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<div class="activity-item"> |
|
|
<div class="activity-dot"></div> |
|
|
<div class="activity-content"> |
|
|
<div class="activity-text">Document "IOS-QMS-WIN-OZ" was successfully archived</div> |
|
|
<div class="activity-time">2 hours ago</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="activity-item"> |
|
|
<div class="activity-dot"></div> |
|
|
<div class="activity-content"> |
|
|
<div class="activity-text">Workflow for "Draft Safety Manual" is pending approval</div> |
|
|
<div class="activity-time">1 day ago</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">Quick Actions</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<button class="btn btn-primary" style="margin-bottom: 12px; width: 100%;"> |
|
|
{SVG_FLUENT['add']} New Document |
|
|
</button> |
|
|
<button class="btn btn-secondary" style="margin-bottom: 12px; width: 100%;"> |
|
|
{SVG_FLUENT['workflow']} View Workflows |
|
|
</button> |
|
|
<button class="btn btn-secondary" style="width: 100%;"> |
|
|
{SVG_FLUENT['archive']} Browse Archive |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
def generate_list_content(entity_type="Documents"): |
|
|
"""Generate PowerApps-style list content""" |
|
|
return f""" |
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">Active {entity_type}</div> |
|
|
<div class="card-actions"> |
|
|
<button class="btn btn-primary">{SVG_FLUENT['add']} New</button> |
|
|
<button class="btn btn-secondary">Delete</button> |
|
|
<button class="btn btn-secondary">Refresh</button> |
|
|
<button class="btn btn-secondary">Export</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<table class="table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>{entity_type[:-1]} Type</th> |
|
|
<th>Created By</th> |
|
|
<th>Created On</th> |
|
|
<th>Status</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
<tr> |
|
|
<td>SOP</td> |
|
|
<td>Pricing</td> |
|
|
<td>9/12/2025 8:45 PM</td> |
|
|
<td><span class="status-badge status-success">Active</span></td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td>MAN</td> |
|
|
<td>Alain</td> |
|
|
<td>9/12/2025 8:25 PM</td> |
|
|
<td><span class="status-badge status-success">Active</span></td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td>RPT</td> |
|
|
<td>Pricing</td> |
|
|
<td>9/6/2025 10:35 PM</td> |
|
|
<td><span class="status-badge status-warning">Pending</span></td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
def generate_form_content(): |
|
|
"""Generate PowerApps-style form content""" |
|
|
return """ |
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">New Document Type</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<div class="grid grid-2"> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">Document Type</label> |
|
|
<input type="text" class="form-control" placeholder="Enter document type"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">Created On</label> |
|
|
<input type="text" class="form-control" value="9/10/2025" readonly> |
|
|
</div> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">Description</label> |
|
|
<textarea class="form-control" rows="3" placeholder="Enter description"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
<div class="card-footer"> |
|
|
<button class="btn btn-primary">Save and Close</button> |
|
|
<button class="btn btn-secondary">Cancel</button> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def enhance_generated_content(raw_html, screen_type, screen_name): |
|
|
"""Enhance the raw HTML generated by Gemini with proper PowerApps styling""" |
|
|
|
|
|
|
|
|
if not raw_html or "<div" not in raw_html or "card" not in raw_html: |
|
|
if "dashboard" in screen_type.lower() or "home" in screen_name.lower(): |
|
|
return generate_dashboard_content() |
|
|
elif "list" in screen_type.lower() or "table" in screen_name.lower(): |
|
|
return generate_list_content(screen_name) |
|
|
elif "form" in screen_type.lower() or "new" in screen_name.lower(): |
|
|
return generate_form_content() |
|
|
else: |
|
|
|
|
|
return f""" |
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title">{screen_name}</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
{raw_html if raw_html else "<p>Content will be displayed here</p>"} |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return raw_html |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_business_pdf(pdf_file): |
|
|
text = "" |
|
|
with pdfplumber.open(pdf_file.name) as pdf: |
|
|
for page in pdf.pages: |
|
|
if (t := page.extract_text()): |
|
|
text += t + "\n" |
|
|
|
|
|
|
|
|
user_name = None |
|
|
possible_name_patterns = [ |
|
|
r"Prepared by[:\-]\s*([A-Z][a-z]+(?:\s[A-Z][a-z]+)+)", |
|
|
r"Author[:\-]\s*([A-Z][a-z]+(?:\s[A-Z][a-z]+)+)", |
|
|
r"Created by[:\-]\s*([A-Z][a-z]+(?:\s[A-Z][a-z]+)+)", |
|
|
r"Owner[:\-]\s*([A-Z][a-z]+(?:\s[A-Z][a-z]+)+)" |
|
|
] |
|
|
|
|
|
for pattern in possible_name_patterns: |
|
|
match = re.search(pattern, text) |
|
|
if match: |
|
|
user_name = match.group(1).strip() |
|
|
break |
|
|
|
|
|
if not user_name: |
|
|
user_name = "John Joe" |
|
|
|
|
|
|
|
|
try: |
|
|
app_title = model.generate_content( |
|
|
"From the following business requirements, infer a concise PowerApp title. Return only the title.\n\n" + text |
|
|
).text.strip() |
|
|
except Exception: |
|
|
app_title = "QMS Document Management" |
|
|
|
|
|
|
|
|
try: |
|
|
prompt = ( |
|
|
"You are a senior PowerApps architect. Analyze the business requirements and return JSON structure like:\n" |
|
|
"[" |
|
|
" {\"group\": \"My Work\", \"screens\": [" |
|
|
" {\"screen_name\": \"Dashboard\", \"role\": \"dashboard\", \"screen_type\": \"dashboard\", \"html\": \"<div>...</div>\"}," |
|
|
" {\"screen_name\": \"Document List\", \"role\": \"list\", \"screen_type\": \"list\", \"html\": \"<div>...</div>\"}" |
|
|
" ]}" |
|
|
"]\n" |
|
|
"- Identify user roles (dashboard, list, form, settings, etc.)\n" |
|
|
"- Group screens logically (My Work, Metadata, Administration, etc.)\n" |
|
|
"- Generate appropriate HTML content for each screen\n" |
|
|
"- Use PowerApps-style components: cards, grids, tables, forms\n" |
|
|
"- Return valid JSON only.\n\n" |
|
|
f"Document:\n{text}" |
|
|
) |
|
|
|
|
|
response = model.generate_content(prompt) |
|
|
cleaned = re.sub(r"```(?:json)?|```", "", response.text.strip()) |
|
|
groups = eval(cleaned) if cleaned.strip().startswith("[") else [] |
|
|
|
|
|
except Exception as e: |
|
|
print("⚠️ Gemini analysis failed:", e) |
|
|
|
|
|
groups = [{ |
|
|
"group": "My Work", |
|
|
"screens": [ |
|
|
{"screen_name": "Dashboard", "role": "dashboard", "screen_type": "dashboard", "html": ""}, |
|
|
{"screen_name": "My Submissions", "role": "list", "screen_type": "list", "html": ""}, |
|
|
{"screen_name": "Document Archive", "role": "list", "screen_type": "list", "html": ""} |
|
|
] |
|
|
}] |
|
|
|
|
|
return app_title, groups, user_name |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_mockups(app_title, groups, user_name): |
|
|
image_paths = [] |
|
|
|
|
|
for group in groups: |
|
|
for screen in group.get("screens", []): |
|
|
label = screen.get("screen_name", "Screen") |
|
|
screen_type = screen.get("screen_type", "dashboard") |
|
|
role = screen.get("role", "default") |
|
|
raw_html = screen.get("html", "") |
|
|
|
|
|
|
|
|
enhanced_content = enhance_generated_content(raw_html, screen_type, label) |
|
|
|
|
|
sidebar_html = generate_sidebar(app_title, groups, active_label=label) |
|
|
topbar_html = generate_topbar(label, role=role, user_name=user_name) |
|
|
|
|
|
full_html = BASE_TEMPLATE.replace("{user_sidebar}", sidebar_html)\ |
|
|
.replace("{user_content}", enhanced_content)\ |
|
|
.replace("{user_topbar}", topbar_html) |
|
|
|
|
|
uid = str(uuid.uuid4())[:8] |
|
|
html_path = f"mockup_{label.replace(' ', '_')}_{uid}.html" |
|
|
img_path = f"mockup_{label.replace(' ', '_')}_{uid}.png" |
|
|
|
|
|
with open(html_path, "w", encoding="utf-8") as f: |
|
|
f.write(full_html) |
|
|
|
|
|
hti.screenshot(html_file=html_path, save_as=img_path) |
|
|
image_paths.append(img_path) |
|
|
|
|
|
return image_paths |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_from_pdf(pdf_file): |
|
|
for attempt in range(3): |
|
|
try: |
|
|
app_title, groups, user_name = analyze_business_pdf(pdf_file) |
|
|
return generate_mockups(app_title, groups, user_name) |
|
|
except Exception as e: |
|
|
if "429" in str(e) or "quota" in str(e).lower(): |
|
|
print("⏳ Waiting for Gemini quota reset... retrying in 10 seconds") |
|
|
time.sleep(10) |
|
|
else: |
|
|
raise e |
|
|
raise Exception("❌ Failed after 3 retries due to Gemini API quota limits.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
gr.Markdown("## 🧩 Intelligent PowerApps Mockup Generator (Enhanced Multi-Screen Mode)") |
|
|
pdf_input = gr.File(label="📄 Upload Business Requirement PDF", file_types=[".pdf"]) |
|
|
generate_btn = gr.Button("🚀 Generate Mockups") |
|
|
gallery_output = gr.Gallery(label="Generated Screens", show_label=True, columns=2) |
|
|
|
|
|
generate_btn.click(fn=generate_from_pdf, inputs=pdf_input, outputs=gallery_output) |
|
|
|
|
|
demo.launch(server_name="0.0.0.0", server_port=7860) |