""",
}
# ============================================================
# BLOCK HELPERS
# ============================================================
def _uid():
return str(uuid.uuid4())[:6]
def _block_label(block):
return f"{block['type']} [{block['id']}]"
def _find_block(label):
for b in page_blocks:
if _block_label(b) == label:
return b
return None
def get_block_choices():
return [_block_label(b) for b in page_blocks]
# ============================================================
# HuggingFace LOCAL INFERENCE
# ============================================================
_hf_models = {}
def load_hf_model(model_id):
if model_id not in _hf_models:
print(f"Loading {model_id}...")
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
device_map="auto",
)
_hf_models[model_id] = (tokenizer, model)
print(f"✅ {model_id} loaded.")
return _hf_models[model_id]
def _ollama_chat(model, system_prompt, user_message, temperature=0.3):
"""Inner implementation — safe to call from within a @spaces.GPU context."""
if not TORCH_AVAILABLE:
return "ERROR: torch/transformers not installed."
try:
tokenizer, hf_model = load_hf_model(model)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(hf_model.device)
max_new_tokens = 1024 if model == QWEN_CODER_MODEL else 512
with torch.no_grad():
outputs = hf_model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature if temperature > 0 else None,
do_sample=temperature > 0,
pad_token_id=tokenizer.eos_token_id,
)
return tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
except Exception as e:
return f"ERROR: {e}"
@spaces.GPU
def ollama_chat(model, system_prompt, user_message, temperature=0.3):
return _ollama_chat(model, system_prompt, user_message, temperature)
def parse_json_from_response(text):
m = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
if m:
text = m.group(1)
try:
return json.loads(text.strip())
except json.JSONDecodeError:
m2 = re.search(r'\{.*\}', text, re.DOTALL)
if m2:
try:
return json.loads(m2.group())
except json.JSONDecodeError:
pass
return {}
# ============================================================
# MENU ZIP
# ============================================================
def process_menu_zip(zip_file):
menu = {}
if zip_file is None:
return menu
extract_dir = WORK_DIR / "menu_images"
if extract_dir.exists():
shutil.rmtree(extract_dir)
extract_dir.mkdir(parents=True)
with zipfile.ZipFile(zip_file, 'r') as zf:
zf.extractall(extract_dir)
for root, dirs, files in os.walk(extract_dir):
rel = Path(root).relative_to(extract_dir)
if str(rel).startswith(('__', '.')):
continue
for fname in sorted(files):
if fname.startswith(('.', '__')):
continue
if not fname.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
continue
fpath = Path(root) / fname
parts = fpath.relative_to(extract_dir).parts
category = parts[-2] if len(parts) >= 2 else "Menu"
stem = fpath.stem
last_us = stem.rfind('_')
if last_us > 0:
name_part = stem[:last_us].replace('_', ' ').strip()
try:
price = float(stem[last_us + 1:].strip())
except ValueError:
name_part = stem.replace('_', ' ').strip(); price = 0.0
else:
name_part = stem.replace('_', ' ').strip(); price = 0.0
with open(fpath, 'rb') as f:
img_bytes = f.read()
img_b64 = base64.b64encode(img_bytes).decode('utf-8')
ext = fpath.suffix.lower()
mime = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp'}
if category not in menu:
menu[category] = []
menu[category].append({
"name": name_part, "price": price,
"image_base64": f"data:{mime.get(ext,'image/png')};base64,{img_b64}",
})
return menu
# ============================================================
# MENU HTML BUILDERS
# ============================================================
def build_category_tabs(menu_data):
return "\n ".join(
f'
{cat}
'
for cat in menu_data.keys()
)
def build_menu_html(menu_data):
html = ""
for cat, items in menu_data.items():
html += f'
\n'
html += f'
{cat}
\n'
for item in items:
price_str = f"${item['price']:.2f}" if item['price'] > 0 else ""
img_src = item.get('image_base64', '')
img_tag = (f''
if img_src else
'')
html += (current_menu_item_template
.replace("{name}", item["name"])
.replace("{price}", price_str)
.replace("{img_tag}", img_tag)
.replace("{img_src}", img_src)
.replace("{description}", item.get("description", "")) + "\n")
html += '
\n'
return html
# ============================================================
# TEMPLATE ENGINE
# ============================================================
def load_template(key):
path = TEMPLATE_DIR / f"{key}.html"
if not path.exists():
path = TEMPLATE_DIR / "warm.html"
return path.read_text(encoding='utf-8')
def rebuild_html():
global current_html
template = load_template(current_template_key)
home_html = "\n".join(b["html"] for b in page_blocks)
html = template
html = html.replace("", home_html)
html = html.replace("", build_category_tabs(current_menu_data))
html = html.replace("", build_menu_html(current_menu_data))
html = html.replace("{{SHOP_NAME}}", current_site_info.get("shop_name", "My Shop"))
# Poster carousel injection (into any .promo-placeholder found in blocks)
posters = current_site_info.get("posters", [])
if posters:
if len(posters) == 1:
carousel_html = f''
else:
slides = "\n".join(
f'
'
for i, p in enumerate(posters)
)
dots = "\n".join(
f''
for i in range(len(posters))
)
carousel_html = (
f'
{slides}'
f''
f''
f'
{dots}
'
)
pat = r'
]*>.*?
'
html = re.sub(pat, carousel_html, html, flags=re.DOTALL, count=1)
carousel_css = (
".ps-wrap{position:relative;overflow:hidden;border-radius:var(--card-radius,12px);}"
".ps-slide{display:none;}.ps-slide.ps-active{display:block;}"
".ps-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.45);"
"color:#fff;border:none;padding:10px 14px;font-size:18px;cursor:pointer;z-index:10;border-radius:6px;}"
".ps-l{left:8px;}.ps-r{right:8px;}"
".ps-dots{position:absolute;bottom:10px;width:100%;text-align:center;}"
".ps-dot{display:inline-block;width:8px;height:8px;background:rgba(255,255,255,0.5);"
"border-radius:50%;margin:0 3px;cursor:pointer;}"
".ps-dot.ps-dot-on{background:#fff;}"
)
carousel_js = (
""
)
html = html.replace("", f"\n/* Carousel */\n{carousel_css}\n", 1)
html = html.replace(", or
- Then make the item itself a compact vertical block (display:block, not flex row)
GENERAL RULES:
- Output ONLY the modified HTML template. No explanation, no markdown fences, no ```html.
- Keep {name}, {price}, and at least one of {img_tag} or {img_src}
- For UI decorations (stars, spice icons, badges): write them as static HTML/emoji, NOT as a placeholder
- Use CSS variables for colors (--primary, --bg-card, --text, --text-muted, --border)
- Do NOT add , , or tags. A single
", f"\n{carousel_js}\n", 1)
# Custom CSS overrides
css = current_site_info.get("custom_css", "")
if css:
html = html.replace("", f"\n/* Custom */\n{css}\n", 1)
current_html = html
return html
# ============================================================
# BLOCK MANAGEMENT HANDLERS
# ============================================================
def _make_menu_preview_html():
if not current_menu_data:
return BLOCK_DEFAULTS["Menu Preview"]
preview_items = []
for items in current_menu_data.values():
for item in items:
preview_items.append(item)
if len(preview_items) >= 3:
break
if len(preview_items) >= 3:
break
rows = ""
for item in preview_items:
price_str = f"${item['price']:.2f}" if item.get('price', 0) > 0 else ""
img_src = item.get('image_base64', '')
img_html = (f''
if img_src else
'
')
rows += f"""
{img_html}
{item['name']}
{price_str}
\n"""
return f"""\
Our Menu
{rows}
"""
def handle_add_block(block_type, selected_label):
if block_type == "Menu Preview":
initial_html = _make_menu_preview_html()
else:
initial_html = BLOCK_DEFAULTS.get(block_type, "")
new_block = {"id": _uid(), "type": block_type, "html": initial_html}
if selected_label and _find_block(selected_label):
idx = next((i for i, b in enumerate(page_blocks) if _block_label(b) == selected_label), -1)
page_blocks.insert(idx + 1, new_block)
else:
page_blocks.append(new_block)
rebuild_html()
choices = get_block_choices()
new_label = _block_label(new_block)
return preview(current_html), gr.update(choices=choices, value=new_label)
def handle_remove_block(selected_label):
global page_blocks
if not selected_label:
return preview(current_html), gr.update()
page_blocks = [b for b in page_blocks if _block_label(b) != selected_label]
rebuild_html()
choices = get_block_choices()
return preview(current_html), gr.update(choices=choices, value=choices[0] if choices else None)
def handle_move_up(selected_label):
idx = next((i for i, b in enumerate(page_blocks) if _block_label(b) == selected_label), -1)
if idx > 0:
page_blocks[idx - 1], page_blocks[idx] = page_blocks[idx], page_blocks[idx - 1]
rebuild_html()
return preview(current_html), gr.update(choices=get_block_choices(), value=selected_label)
def handle_move_down(selected_label):
idx = next((i for i, b in enumerate(page_blocks) if _block_label(b) == selected_label), -1)
if 0 <= idx < len(page_blocks) - 1:
page_blocks[idx], page_blocks[idx + 1] = page_blocks[idx + 1], page_blocks[idx]
rebuild_html()
return preview(current_html), gr.update(choices=get_block_choices(), value=selected_label)
# ============================================================
# LLM BLOCK EDITOR
# ============================================================
CODER_SYSTEM = """You are a frontend developer editing a mobile website HTML block.
RULES:
- Output ONLY the modified HTML. No explanation, no markdown fences, no ```html.
- Keep the mobile-friendly layout and existing CSS classes.
- Only change what the instruction asks.
- Do NOT output ,