import json import os import re from collections import OrderedDict from html import escape from pathlib import Path import gradio as gr import uvicorn from fastapi import FastAPI from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from meltmind_engine import MeltMindEngine from space_runtime import backend_status, start_space_backend ROOT = Path(__file__).parent MENU = json.loads((ROOT / "data" / "menu.json").read_text()) ADD_ONS = json.loads((ROOT / "data" / "meltmind" / "add_ons.json").read_text())["add_ons"] INGREDIENTS = json.loads((ROOT / "data" / "meltmind" / "ingredients.json").read_text())["ingredients"] BUSINESS = json.loads((ROOT / "data" / "meltmind" / "business_profile.json").read_text()) POLICIES = json.loads((ROOT / "data" / "meltmind" / "operations_and_policies.json").read_text()) CSS = (ROOT / "styles.css").read_text() ENGINE = MeltMindEngine() SPACE_BACKEND = start_space_backend() LOGO_PATH = "assets/brand/Meltroom.jpeg" HERO_PATH = "assets/hero/meltroom_special1.jpg" WHATSAPP_NUMBER = "917780534935" class ChatRequest(BaseModel): message: str = Field(min_length=1, max_length=2000) history: list = Field(default_factory=list) class DesignerRequest(BaseModel): budget_inr: int = Field(default=500, ge=99, le=50000) group_size: int = Field(default=1, ge=1, le=50) flavours: list[str] = Field(default_factory=list) textures: list[str] = Field(default_factory=list) premium: bool = False variety: bool = False less_sweet: bool = False allergen_ids: list[str] = Field(default_factory=list) severe_allergy: bool = False request: str = "" def asset_url(path: str) -> str: return f"/{path}" def numeric_price(value) -> int: match = re.search(r"\d+", str(value)) return int(match.group()) if match else 0 def slug(value: str) -> str: return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") def add_on_matches_product(add_on: dict, product: dict) -> bool: product_id = slug(product["name"]) explicit_ids = {slug(value) for value in add_on.get("compatible_product_ids", [])} if product_id in explicit_ids: return True if product["category"] in add_on.get("compatible_categories", []): return True aliases = { "choco_chips": ("chocolate chips", "extra chocolate chips"), "chocolate_sauce": ("chocolate sauce", "extra chocolate drizzle"), "white_chocolate_sauce": ("white chocolate sauce", "extra white chocolate drizzle"), "pista": ("extra pistachio filling", "extra pistachio cream", "pistachio"), "kunafa": ("kunafa",), "banana_slices": ("extra banana", "banana slices"), "mango": ("extra mango", "fresh mango"), "dark_chocolate_filling": ("extra dark chocolate", "dark chocolate filling"), "brownie_crumbs": ("brownie chunks", "brownie crumbs"), "whipped_cream": ("extra cream filling", "whipped cream"), } listed = {value.strip().lower() for value in product["ordering_information"].get("available_add_ons", [])} return any(alias in listed for alias in aliases.get(add_on["add_on_id"], (add_on["name"].lower(),))) def js_data() -> str: products = [] for item in MENU: compatible_add_ons = [ add_on["add_on_id"] for add_on in ADD_ONS if add_on["availability"] == "available" and add_on_matches_product(add_on, item) ] products.append( { "id": slug(item["name"]), "name": item["name"], "category": item["category"], "price": numeric_price(item["price"]), "image": asset_url(item["image"]) if item.get("image") else "", "tagline": item.get("tagline", ""), "description": item.get("detailed_description") or item.get("description", ""), "short": item.get("one_line_description") or item.get("description", ""), "serves": item.get("can_be_served_for", ""), "sweetness": item["taste_profile"].get("sweetness", ""), "chocolate": item["taste_profile"].get("chocolate_intensity", ""), "textures": item["taste_profile"].get("textures", []), "flavours": item["taste_profile"].get("flavours", []), "bestFor": [value for value in item["taste_profile"].get("best_for", []) if value], "tags": item.get("tags", []), "availableAddOns": compatible_add_ons, } ) add_ons = [ { "id": item["add_on_id"], "name": item["name"], "price": item["price_inr"], "description": item["description"], "flavours": item["flavour_contribution"], "textures": item["texture_contribution"], "allergens": item["allergen_ids"], "premium": item["premium_add_on"], "compatibleProductIds": [slug(value) for value in item.get("compatible_product_ids", [])], "compatibleCategories": item.get("compatible_categories", []), "message": item.get("upsell_message", ""), } for item in ADD_ONS if item["availability"] == "available" ] return json.dumps({"products": products, "addOns": add_ons}, ensure_ascii=False).replace(" str: product_id = slug(item["name"]) image = asset_url(item["image"]) if item.get("image") else "" flavours = " · ".join(item["taste_profile"].get("flavours", [])[:2]) badges = [ f'{escape(item.get("can_be_served_for", ""))}', "Gluten-free", "Eggless", ] if "tree_nuts" in item["dietary_information"].get("allergens", []): badges.append("Contains nuts") return f"""
{escape(flavours)}₹{numeric_price(item["price"])}

{escape(item.get("tagline", ""))}

{"".join(badges)}
""" def menu_markup() -> str: grouped = OrderedDict() for item in MENU: grouped.setdefault(item["category"], []).append(item) navigation = "".join( f'' f'{CATEGORY_DETAILS[category][0]}
{escape(category)}{escape(CATEGORY_DETAILS[category][1])}
Explore ↘
' for category in grouped ) sections = [] for category, items in grouped.items(): number, title, copy = CATEGORY_DETAILS[category] sections.append( f""" """ ) return f""" """ def add_on_markup() -> str: cards = [] for item in ADD_ONS: if item["availability"] != "available": continue labels = item["texture_contribution"][:1] + item["flavour_contribution"][:1] allergen = " · Contains " + ", ".join(item["allergen_ids"]) if item["allergen_ids"] else "" cards.append( f"""
{"Premium finish" if item["premium_add_on"] else "Finishing touch"}+₹{item["price_inr"]}

{escape(item["name"])}

{escape(item["description"])}

{"".join(f"{escape(value)}" for value in labels)}{f"{escape(allergen)}" if allergen else ""}
""" ) return f"""

Finish it your way

The final
little obsession.

Add extra chocolate, cream, fruit, pistachio, or crunch. MeltBasket keeps every finishing touch clear and separate.

{"".join(cards)}
""" def ingredient_markup() -> str: wanted = ["brownie_base", "premium_dark_chocolate", "chocolate_chips", "pista_cream", "kunafa", "honey"] by_id = {item["ingredient_id"]: item for item in INGREDIENTS} cards = [] for index, ingredient_id in enumerate(wanted, start=1): item = by_id[ingredient_id] flavours = item.get("flavour_contribution", [])[:2] textures = item.get("texture_contribution", [])[:2] labels = item.get("dietary_labels", [])[:3] cards.append( f"""
{index:02}{escape(item["ingredient_type"].replace("_", " "))}

{escape(item["name"])}

{escape(item["plain_explanation"])}

Flavour{escape(" · ".join(flavours) or "Complements the dessert")}
Texture{escape(" · ".join(textures) or "Supports the final finish")}
{"".join(f"{escape(value.replace('_', ' '))}" for value in labels)}
Why it belongs{escape(item["why_meltroom_uses_it"])}
""" ) return f"""

Inside every MeltRoom creation

Every layer
has a purpose.

Explore the ingredients that shape MeltRoom's flavour, texture, and character. Our brownie base is gluten-free and eggless, made without maida or refined sugar, then finished with premium chocolate, fruits, creams, and thoughtful contrasts.

Gluten-free browniesEgglessNo maidaNo refined sugar
Substitutions, thoughtfully handledOat or almond milk may replace milk where compatible. Almond milk contains tree nuts, and complete allergy requirements must always be confirmed with MeltRoom.
Ingredient libraryFlavour. Texture. Purpose.Hover or scroll through the building blocks behind the MeltRoom experience.
{"".join(cards)}
""" def meltmind_markup() -> str: return """

MeltMind AI · Choose your experience

How would you like
to find your melt?

Your informed dessert concierge.

Ask anything about MeltRoom, compare products, understand flavours and textures, explore ingredients, or get thoughtful guidance in natural conversation.

Best when you want to explore, ask questions, or compare before deciding.

A complete order shaped around you.

Choose your flavour, texture, people, budget, and moment. MeltMind creates multiple valid plans with clear prices and serving guidance.

Best when you want a complete recommendation or group order.
""" HEAD = f""" """ logo = f'MeltRoom logo' if (ROOT / LOGO_PATH).exists() else "M" hero = f'MeltRoom signature dessert' if (ROOT / HERO_PATH).exists() else "" def app_markup() -> str: return f"""

MeltRoom · Signature dessert studio

Not just dessert.
Your kind of melt.

Gluten-free, eggless brownie experiences built around rich chocolate, playful formats, and the mood you arrived with.

18 signature creations4 dessert chapters100% gluten-free brownies
{hero}
Made for cravings worth rememberingSignature brownie bowlsPlayful melts, serious chocolatePremium ingredients, beautifully layeredYour moment, your perfect meltMade for cravings worth rememberingSignature brownie bowlsPlayful melts, serious chocolatePremium ingredients, beautifully layeredYour moment, your perfect melt
{menu_markup()} {add_on_markup()} {meltmind_markup()} {ingredient_markup()}

Your journey through this experience

From first craving
to final melt.

Everything happens in one clear flow: discover, understand, customize, confirm, and receive.

01Discover

Explore the four menu chapters

Open product cards to understand flavour, texture, serving guidance, and price before choosing.

Explore menu ↗
02Get guidance

Ask MeltMind or design your melt

Ask questions naturally, compare options, or create a complete plan around your craving, people, and budget.

Meet MeltMind ↗
03Customize

Build your MeltBasket

Add products, adjust quantities, and attach compatible finishing touches to the exact dessert you want.

04Confirm

Send the prepared order on WhatsApp

Place Order creates a clean WhatsApp message. MeltRoom reviews availability and shares the owner's payment QR.

05Receive

Payment confirms your order

After payment, MeltRoom updates the order and delivery status. Choose Rapido delivery or pickup near KLN Reddy Colony.

Regular ordersGenerally targeted within 45 minutes after confirmation.Delivery chargesRapido charge is extra. Above ₹100, MeltRoom contributes 40%.Large ordersDiscuss approximately 5–6 hours ahead.Best tasteEnjoy within 60 minutes or refrigerate and gently reheat.

Stay close to MeltRoom

See the melts.
Share the love.

Follow new creations and behind-the-scenes moments, leave a Google review after your order, or speak directly with MeltRoom for support and custom requests.

""" def html_page() -> str: return f""" MeltRoom · MeltMind AI {HEAD} {app_markup()} """ with gr.Blocks(title="MeltRoom · MeltMind AI", css=CSS, head=HEAD, theme=gr.themes.Base(), elem_classes="meltmind-app") as demo: gr.HTML(app_markup(), container=False) web_app = FastAPI(title="MeltMind AI") web_app.mount("/assets", StaticFiles(directory=ROOT / "assets"), name="assets") @web_app.get("/", response_class=HTMLResponse) def meltmind_home(): return HTMLResponse(html_page()) @web_app.post("/api/meltmind/chat") def meltmind_chat(request: ChatRequest): return ENGINE.chat(request.message, request.history) @web_app.post("/api/meltmind/design") def design_perfect_melt(request: DesignerRequest): return ENGINE.design(request.model_dump()) @web_app.get("/api/meltmind/model") def meltmind_model_status(): return {**ENGINE.runtime_status(check_connection=True), "backend": backend_status()} web_app = gr.mount_gradio_app(web_app, demo, path="/gradio", allowed_paths=[str(ROOT / "assets")]) if __name__ == "__main__": uvicorn.run(web_app, host="0.0.0.0", port=int(os.getenv("GRADIO_SERVER_PORT", "7860")))