"""
def make_gallery_html(templates: list, user_apps: list = None) -> str:
grid_icon = ''
cards = ""
# User-built apps first (highlighted) — ONLY show user apps if they exist
if user_apps and len(user_apps) > 0:
for app_data in user_apps:
tags_html = "".join(f'{t}' for t in app_data.get("tags", ["custom"]))
app_url = app_data.get("url", "")
onclick_attr = f'onclick="window.viewAppInPreview(\'{app_url}\')"' if app_url else ""
cards += f"""
✦
{app_data["name"]}
{app_data.get("desc", "Built with Alembic")}
{tags_html}
View App →
"""
# Default templates — only show if no user apps
if not user_apps or len(user_apps) == 0:
for app_data in templates:
tags_html = "".join(f'{t}' for t in app_data["tags"])
app_icon = icon(app_data["icon"], "gallery-card-icon")
template_desc = app_data["desc"]
cards += f"""
{app_icon}
{app_data["name"]}
{app_data["desc"]}
{tags_html}
Use Template →
"""
cards += f"""
{icon("plus", "gallery-card-icon")}
New App
Start from scratch.
Create New App →
"""
# Change header based on content
if user_apps and len(user_apps) > 0:
header_title = "Your Apps"
header_subtitle = f"{len(user_apps)} app{'s' if len(user_apps) > 1 else ''} built with Alembic."
else:
header_title = "Templates"
header_subtitle = "Kickstart your next project with a template."
return f"""
"""
# ---------------------------------------------------------------------------
# Chat history rendering
# ---------------------------------------------------------------------------
def render_chat_html(chat_history: list) -> str:
if not chat_history:
return ""
html_parts = []
for msg in chat_history:
role = msg["role"]
content = msg["content"]
if role == "user":
html_parts.append(
f'
'
f'
{content}
'
f'
'
)
else:
html_parts.append(
f'
'
f'
{content}
'
f'
'
)
return f'
{"".join(html_parts)}
'
# ---------------------------------------------------------------------------
# Main handler — chat-driven with history
# ---------------------------------------------------------------------------
async def handle_generate(
user_message: str,
js_user_id: str,
chat_html: str,
preview_frame: str,
gallery_html: str,
session_state: dict,
):
"""Process user message: show in chat, call agent, stream response, build if triggered."""
HIDE_SAVE = gr.update(visible=False)
SHOW_SAVE = gr.update(visible=True)
if not user_message.strip():
yield chat_html, preview_frame, gallery_html, session_state, "", HIDE_SAVE
return
# Use persistent user_id from localStorage (via JS hidden field)
if js_user_id and js_user_id.strip():
session_state["user_id"] = js_user_id.strip()
elif not session_state.get("user_id"):
session_state["user_id"] = str(uuid.uuid4())
if "agent_state" not in session_state:
session_state["agent_state"] = None
if "chat_history" not in session_state:
session_state["chat_history"] = []
if "built_apps" not in session_state:
session_state["built_apps"] = []
# Add user message to history
session_state["chat_history"].append({"role": "user", "content": user_message})
# Show user message + thinking indicator
thinking_history = session_state["chat_history"] + [
{"role": "assistant", "content": ' Analyzing...'}
]
yield render_chat_html(thinking_history), preview_frame, gallery_html, session_state, "", HIDE_SAVE
await asyncio.sleep(0.3)
# Call agent backend
try:
response = await call_backend_agent(user_message, session_state["user_id"], session_state)
except Exception as e:
print(f"Backend error: {e}, falling back to mock")
try:
response = await mock_agent_response(user_message, session_state)
except Exception as e2:
error_msg = f"Connection issue — retrying... ({str(e)[:60]})"
session_state["chat_history"].append({"role": "assistant", "content": error_msg})
yield render_chat_html(session_state["chat_history"]), preview_frame, gallery_html, session_state, "", HIDE_SAVE
return
response_text = response.get("response", "")
new_agent_state = response.get("new_state", {})
actions = response.get("actions", [])
resp_confidence = response.get("confidence", 0.5)
plan = response.get("plan")
session_state["agent_state"] = new_agent_state
assistant_content = response_text
resp_pct = int(resp_confidence * 100)
assistant_content += f' Confidence: {resp_pct}%'
session_state["chat_history"].append({"role": "assistant", "content": assistant_content})
yield render_chat_html(session_state["chat_history"]), preview_frame, gallery_html, session_state, "", HIDE_SAVE
# --- Service connection: check on every turn ---
all_required_services = []
for action in actions:
if action.get("type") == "show_connect":
all_required_services.extend(action.get("required_services", []))
if action.get("type") == "start_build":
all_required_services.extend(action.get("required_services", []))
all_required_services = list(set(all_required_services))
if all_required_services and not session_state.get("services_connected"):
try:
from model_runtime import _post
service_status = await _post("/check-services", {
"user_id": session_state["user_id"],
"services": all_required_services
})
all_connected = all(service_status.get("connected", {}).values())
if not all_connected:
service_panel = make_service_connection_html(all_required_services, service_status)
session_state["pending_build"] = {
"requirements": user_message,
"required_services": all_required_services
}
service_names = ", ".join(s.replace("_", " ").title() for s in all_required_services)
session_state["chat_history"].append({
"role": "assistant",
"content": f"🔌 This app needs {service_names} access. Please connect the required services to continue."
})
yield render_chat_html(session_state["chat_history"]), service_panel, gallery_html, session_state, "", HIDE_SAVE
return
else:
session_state["services_connected"] = True
except Exception as e:
print(f"Service check error: {e}")
# Check if build was triggered
build_triggered = any(a.get("type") == "start_build" for a in actions)
if build_triggered:
await asyncio.sleep(0.3)
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Generating code...", message="Writing your application...", progress=20
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
requirements = user_message
design_prefs = {}
for action in actions:
if action.get("type") == "start_build":
requirements = action.get("requirements", user_message)
design_prefs = action.get("design_prefs", {})
break
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Generating code...", message="Building application logic...", progress=50
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
try:
# Pass connected services so codegen injects the API helper
services = (session_state.get("pending_build", {}).get("required_services")
or all_required_services or [])
result = await call_backend_generate(requirements, design_prefs, session_state["user_id"], services)
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Deploying...", message="Uploading to CloudFront...", progress=85
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
await asyncio.sleep(0.5)
code = result.get("code", "")
app_id = result.get("app_id", f"app-{uuid.uuid4().hex[:8]}")
cloudfront_url = result.get("url")
if code and cloudfront_url:
app_name = requirements.strip().split("\n")[0][:40]
if len(app_name) > 30:
app_name = app_name[:27] + "..."
new_preview = IFRAME_PREVIEW_TEMPLATE.format(url=cloudfront_url)
new_app = {
"id": app_id,
"name": app_name,
"desc": "Built with Alembic",
"tags": ["custom", "ai-generated"],
"code": code,
"url": cloudfront_url,
}
session_state["built_apps"].append(new_app)
new_gallery = make_gallery_html(DEFAULT_TEMPLATES, session_state["built_apps"])
session_state["chat_history"].append({
"role": "assistant",
"content": f'✅ Your app {app_name} is live! Deployed'
})
# SUCCESS — show Save button
yield render_chat_html(session_state["chat_history"]), new_preview, new_gallery, session_state, "", SHOW_SAVE
elif code:
new_preview = make_code_preview_html(code, "Generated Code (Deploy Failed)")
session_state["chat_history"].append({
"role": "assistant",
"content": "⚠️ App generated but deploy failed. Code shown in preview."
})
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
else:
session_state["chat_history"].append({
"role": "assistant", "content": "Build didn't produce code. Try being more specific."
})
yield render_chat_html(session_state["chat_history"]), PREVIEW_PLACEHOLDER_HTML, gallery_html, session_state, "", HIDE_SAVE
except Exception as e:
session_state["chat_history"].append({
"role": "assistant", "content": f"Build error: {str(e)[:80]}. Try again."
})
yield render_chat_html(session_state["chat_history"]), PREVIEW_PLACEHOLDER_HTML, gallery_html, session_state, "", HIDE_SAVE
# ---------------------------------------------------------------------------
# Suggestion handler — fills textarea
# ---------------------------------------------------------------------------
def fill_suggestion(suggestion_text: str) -> str:
"""Return the suggestion text to fill into the textarea."""
return suggestion_text
# ---------------------------------------------------------------------------
# Service recheck — called by JS after OAuth popup closes
# ---------------------------------------------------------------------------
async def handle_recheck_services(chat_html, preview_html, gallery_html, session_state):
"""Re-check service connection after OAuth popup and auto-build if ready."""
HIDE_SAVE = gr.update(visible=False)
SHOW_SAVE = gr.update(visible=True)
pending = session_state.get("pending_build", {})
required_services = pending.get("required_services", [])
if not required_services:
yield chat_html, preview_html, gallery_html, session_state, "", HIDE_SAVE
return
try:
from model_runtime import _post
service_status = await _post("/check-services", {
"user_id": session_state["user_id"],
"services": required_services
})
all_connected = all(service_status.get("connected", {}).values())
if not all_connected:
service_panel = make_service_connection_html(required_services, service_status)
yield chat_html, service_panel, gallery_html, session_state, "", HIDE_SAVE
return
session_state["services_connected"] = True
session_state["chat_history"].append({
"role": "assistant", "content": "✅ All services connected! Starting build..."
})
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Generating code...", message="Writing your application...", progress=20
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
requirements = pending.get("requirements", "Build the app")
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Generating code...", message="Building application logic...", progress=50
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
try:
result = await call_backend_generate(requirements, {}, session_state["user_id"], required_services)
new_preview = BUILDING_HTML_TEMPLATE.format(
status="Deploying...", message="Uploading to CloudFront...", progress=85
)
yield render_chat_html(session_state["chat_history"]), new_preview, gallery_html, session_state, "", HIDE_SAVE
await asyncio.sleep(0.5)
code = result.get("code", "")
app_id = result.get("app_id", f"app-{uuid.uuid4().hex[:8]}")
cloudfront_url = result.get("url")
if code and cloudfront_url:
app_name = requirements.strip().split("\n")[0][:40]
if len(app_name) > 30:
app_name = app_name[:27] + "..."
new_preview = IFRAME_PREVIEW_TEMPLATE.format(url=cloudfront_url)
new_app = {
"id": app_id, "name": app_name, "desc": "Built with Alembic",
"tags": ["custom", "ai-generated"], "code": code, "url": cloudfront_url,
}
session_state["built_apps"].append(new_app)
new_gallery = make_gallery_html(DEFAULT_TEMPLATES, session_state["built_apps"])
session_state["chat_history"].append({
"role": "assistant",
"content": f'✅ Your app {app_name} is live! Deployed'
})
yield render_chat_html(session_state["chat_history"]), new_preview, new_gallery, session_state, "", SHOW_SAVE
else:
session_state["chat_history"].append({
"role": "assistant", "content": "Build completed but no output. Try again."
})
yield render_chat_html(session_state["chat_history"]), PREVIEW_PLACEHOLDER_HTML, gallery_html, session_state, "", HIDE_SAVE
except Exception as e:
session_state["chat_history"].append({
"role": "assistant", "content": f"Build error: {str(e)[:80]}. Try again."
})
yield render_chat_html(session_state["chat_history"]), PREVIEW_PLACEHOLDER_HTML, gallery_html, session_state, "", HIDE_SAVE
except Exception as e:
session_state["chat_history"].append({
"role": "assistant", "content": f"Service check error: {str(e)[:80]}. Try connecting again."
})
yield render_chat_html(session_state["chat_history"]), preview_html, gallery_html, session_state, "", HIDE_SAVE
# ---------------------------------------------------------------------------
# CSS & Head
# ---------------------------------------------------------------------------
def load_css() -> str:
css_path = STATIC_DIR / "style.css"
if css_path.exists():
return css_path.read_text()
return ""
HEAD_HTML = """
"""
JS_INIT = """
() => {
document.body.classList.add('alembic-light');
document.documentElement.classList.remove('dark');
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'class') {
if (m.target.classList && m.target.classList.contains('dark')) {
m.target.classList.remove('dark');
}
}
}
});
observer.observe(document.documentElement, { attributes: true });
// Auto-scroll chat area
const chatObserver = new MutationObserver(() => {
const chatEl = document.querySelector('.chat-history');
if (chatEl) chatEl.scrollTop = chatEl.scrollHeight;
});
setTimeout(() => {
const target = document.querySelector('.chat-area');
if (target) chatObserver.observe(target, {childList: true, subtree: true});
}, 1000);
// Persist user_id: cookie (primary) + localStorage (fallback for iframe)
// Cookie with SameSite=Lax; Secure — blocked as 3rd-party in some browsers
// when inside HF Space iframe, so localStorage is the fallback.
function getCookie(name) {
var m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
return m ? m[1] : null;
}
function setCookie(name, val) {
var exp = new Date(Date.now() + 365*24*60*60*1000).toUTCString();
document.cookie = name + '=' + val + '; expires=' + exp + '; path=/; SameSite=Lax; Secure';
}
var userId = getCookie('alembic_uid');
if (!userId) {
try { userId = localStorage.getItem('alembic_user_id'); } catch(e) {}
}
if (!userId) {
userId = 'user-' + crypto.randomUUID();
}
// Write to both stores
setCookie('alembic_uid', userId);
try { localStorage.setItem('alembic_user_id', userId); } catch(e) {}
window.ALEMBIC_USER_ID = userId;
// Fill the hidden user-id field so Python can read it
setTimeout(function() {
var field = document.querySelector('#user-id-store textarea') ||
document.querySelector('#user-id-store input');
if (field) {
field.value = userId;
field.dispatchEvent(new Event('input', { bubbles: true }));
}
}, 800);
}
"""
# ---------------------------------------------------------------------------
# Save app handler
# ---------------------------------------------------------------------------
async def handle_save_app(chat_html, preview_html, gallery_html, session_state):
"""Save the last built app to the global manifest."""
HIDE_SAVE = gr.update(visible=False)
built_apps = session_state.get("built_apps", [])
if not built_apps:
yield chat_html, preview_html, gallery_html, session_state, "", HIDE_SAVE
return
last_app = built_apps[-1]
if last_app.get("saved"):
session_state["chat_history"].append({
"role": "assistant", "content": "This app is already saved."
})
yield render_chat_html(session_state["chat_history"]), preview_html, gallery_html, session_state, "", HIDE_SAVE
return
try:
from model_runtime import _post
await _post("/apps/save", {
"app_id": last_app["id"],
"name": last_app["name"],
"description": last_app.get("desc", ""),
"tags": last_app.get("tags", []),
"url": last_app.get("url", ""),
"user_id": session_state.get("user_id", ""),
})
last_app["saved"] = True
session_state["chat_history"].append({
"role": "assistant",
"content": f'💾 {last_app["name"]} saved to global gallery! Anyone can find it now.'
})
# Hide save button after successful save
yield render_chat_html(session_state["chat_history"]), preview_html, gallery_html, session_state, "", HIDE_SAVE
except Exception as e:
session_state["chat_history"].append({
"role": "assistant", "content": f"Save failed: {str(e)[:80]}. Try again."
})
# Keep save button visible on failure so user can retry
yield render_chat_html(session_state["chat_history"]), preview_html, gallery_html, session_state, "", gr.update(visible=True)
# ---------------------------------------------------------------------------
# Build the Gradio app
# ---------------------------------------------------------------------------
# Pre-load globally saved apps for the gallery
initial_gallery_apps = SAVED_APPS if SAVED_APPS else None
with gr.Blocks(title="Alembic") as demo:
# --- Header Bar ---
gr.HTML(make_header_html(CATEGORIES, "All"), elem_classes="header-wrap")
# --- Session state ---
session_state = gr.State({
"user_id": None,
"agent_state": None,
"chat_history": [],
"built_apps": [],
})
# Hidden user-id field — JS fills from localStorage on page load
user_id_field = gr.Textbox(
value="", elem_id="user-id-store", elem_classes="sr-only-btn",
show_label=False,
)
# --- Main split layout ---
with gr.Row(elem_classes="main-row"):
# LEFT: Chat panel (history + input at bottom)
with gr.Column(scale=46, elem_classes="build-col"):
gr.HTML(make_build_header_html(), elem_classes="build-section")
# Chat history area (scrollable, grows with messages)
chat_area = gr.HTML(value="", elem_classes="chat-area")
# Input area at BOTTOM
user_input = gr.Textbox(
show_label=False,
placeholder="Describe your app...",
lines=3,
max_lines=6,
elem_classes="prompt-area",
)
gr.HTML(make_textarea_footer_html(), elem_classes="textarea-footer-wrap")
with gr.Row(elem_classes="action-buttons-row"):
generate_btn = gr.Button(
"✦ Generate App ⌘↵",
variant="primary",
elem_classes="generate-btn-wrap",
)
save_btn = gr.Button(
"💾 Save App",
variant="secondary",
elem_classes="save-btn-wrap",
visible=False,
)
# Suggestion buttons — clicking fills the textarea
with gr.Row(elem_classes="suggestions-row"):
sug1 = gr.Button("💡 Dashboard for spreadsheet data", elem_classes="suggestion-btn")
sug2 = gr.Button("💡 Form that notifies my team", elem_classes="suggestion-btn")
sug3 = gr.Button("💡 Tool to summarize meeting notes", elem_classes="suggestion-btn")
# RIGHT: Preview panel
with gr.Column(scale=54, elem_classes="preview-col"):
gr.HTML(make_preview_header_html(), elem_classes="preview-label-wrap")
preview_frame = gr.HTML(
value=PREVIEW_PLACEHOLDER_HTML,
elem_classes="preview-frame",
)
# --- Templates / Saved Apps Gallery ---
gallery_section = gr.HTML(
make_gallery_html(DEFAULT_TEMPLATES, initial_gallery_apps),
elem_classes="gallery-section",
)
# Recheck button — CSS-hidden (not visible=False, which removes from DOM).
recheck_btn = gr.Button("Recheck Services", elem_id="recheck-services-btn", elem_classes="sr-only-btn")
# --- Event handlers ---
outputs = [chat_area, preview_frame, gallery_section, session_state, user_input, save_btn]
gen_inputs = [user_input, user_id_field, chat_area, preview_frame, gallery_section, session_state]
generate_btn.click(
fn=handle_generate,
inputs=gen_inputs,
outputs=outputs,
)
user_input.submit(
fn=handle_generate,
inputs=gen_inputs,
outputs=outputs,
)
recheck_btn.click(
fn=handle_recheck_services,
inputs=[chat_area, preview_frame, gallery_section, session_state],
outputs=outputs,
)
save_btn.click(
fn=handle_save_app,
inputs=[chat_area, preview_frame, gallery_section, session_state],
outputs=outputs,
)
# Suggestion buttons fill textarea
sug1.click(fn=lambda: "Dashboard for spreadsheet data", outputs=user_input)
sug2.click(fn=lambda: "Form that notifies my team", outputs=user_input)
sug3.click(fn=lambda: "Tool to summarize meeting notes", outputs=user_input)
# ---------------------------------------------------------------------------
# Launch
# ---------------------------------------------------------------------------
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
css=load_css(),
head=HEAD_HTML,
js=JS_INIT,
)