import streamlit as st import google.generativeai as genai import os from pathlib import Path import json import time from dotenv import load_dotenv import re import urllib.parse st.set_page_config(layout="wide", page_title="Vibe Coding for WebD") load_dotenv() WORKSPACE_DIR = Path("workspace") WORKSPACE_DIR.mkdir(exist_ok=True) CSS_FILENAME = "style.css" try: api_key = os.getenv("GOOGLE_API_KEY") if not api_key: st.error("🔴 Google API Key not found. Please ensure GOOGLE_API_KEY is set in your .env file.") st.stop() genai.configure(api_key=api_key) model_name = os.getenv("GEMINI_MODEL", "gemini-2.5-pro-exp-03-25") st.sidebar.caption(f"Using Model: `{model_name}`") model = genai.GenerativeModel(model_name) except Exception as e: st.error(f"🔴 Failed to configure Gemini or load model '{model_name}': {e}") st.stop() if "messages" not in st.session_state: st.session_state.messages = [] if "selected_file" not in st.session_state: st.session_state.selected_file = None if "file_content" not in st.session_state: st.session_state.file_content = "" if "rendered_html" not in st.session_state: st.session_state.rendered_html = "" def get_workspace_files(): try: return sorted([f.name for f in WORKSPACE_DIR.iterdir() if f.is_file()]) except Exception as e: st.error(f"Error listing workspace files: {e}"); return [] def read_file_content(filename): if not filename: return None if ".." in filename or filename.startswith(("/", "\\")): return None filepath = WORKSPACE_DIR / filename try: with open(filepath, "r", encoding="utf-8") as f: return f.read() except FileNotFoundError: return None except Exception as e: st.error(f"Error reading file '{filename}': {e}"); return None def save_file_content(filename, content): if not filename: return False if ".." in filename or filename.startswith(("/", "\\")): return False filepath = WORKSPACE_DIR / filename try: filepath.parent.mkdir(parents=True, exist_ok=True) with open(filepath, "w", encoding="utf-8") as f: f.write(content); return True except Exception as e: st.error(f"Error saving file '{filename}': {e}"); return False def delete_file(filename): if not filename: return False if ".." in filename or filename.startswith(("/", "\\")): return False filepath = WORKSPACE_DIR / filename try: os.remove(filepath) if st.session_state.selected_file == filename: # Clear state if selected file is deleted st.session_state.selected_file = None st.session_state.file_content = "" st.session_state.rendered_html = "" st.session_state.pop(f"rendered_for_{filename}", None) return True except FileNotFoundError: st.warning(f"File '{filename}' not found for deletion."); return False except Exception as e: st.error(f"Error deleting file '{filename}': {e}"); return False def parse_and_execute_commands(ai_response_text): parsed_commands = [] try: response_text_cleaned = ai_response_text.strip() if response_text_cleaned.startswith("```json"): response_text_cleaned = response_text_cleaned[7:-3].strip() elif response_text_cleaned.startswith("```"): response_text_cleaned = response_text_cleaned[3:-3].strip() commands = json.loads(response_text_cleaned) # Strict parsing if not isinstance(commands, list): return [{"action": "chat", "content": f"AI (Non-list JSON): {ai_response_text}"}] for command in commands: if not isinstance(command, dict): parsed_commands.append({"action": "chat", "content": f"Skipped: {command}"}); continue action=command.get("action"); filename=command.get("filename"); content=command.get("content") parsed_commands.append(command) if action=="create_update": if filename and content is not None: if not save_file_content(filename, content): st.warning(f"Failed save '{filename}'.") else: st.warning(f"⚠️ Invalid 'create_update': {command}") elif action=="delete": if filename: delete_file(filename) else: st.warning(f"⚠️ Invalid 'delete': {command}") elif action=="chat": pass else: st.warning(f"⚠️ Unknown action '{action}': {command}") return parsed_commands except json.JSONDecodeError as e: st.error(f"🔴 Invalid JSON: {e}\nTxt:\n'{ai_response_text[:500]}...'") return [{"action": "chat", "content": f"AI(Invalid JSON): {ai_response_text}"}] except Exception as e: st.error(f"🔴 Error processing commands: {e}") return [{"action": "chat", "content": f"Error processing commands: {e}"}] def call_gemini(history): safe_history = [] for msg in history: if isinstance(msg, dict) and "role" in msg and "content" in msg: safe_history.append({"role": msg["role"], "content": str(msg["content"])}) instruction = """ You are an AI assistant that helps users create web pages and simple web applications. Your goal is to generate HTML, CSS, JavaScript code, or self-contained React preview files. Based on the user's request, you MUST respond ONLY with a valid JSON array containing file operation objects. **JSON FORMATTING RULES (VERY IMPORTANT):** 1. The entire response MUST be a single JSON array starting with '[' and ending with ']'. 2. All keys (like "action", "filename", "content") MUST be enclosed in **double quotes** ("). 3. All string values (like filenames and the large code content) MUST be enclosed in **double quotes** ("). Single quotes (') or backticks (`) are NOT ALLOWED for keys or string values in the JSON structure. 4. Special characters within the "content" string (like newlines, double quotes inside the code) MUST be properly escaped (e.g., use '\\n' for newlines, '\\"' for double quotes). **EXAMPLE of Correct JSON action object:** { "action": "create_update", "filename": "example.html", "content": "\\n\\n
\\nThis contains a \\"quote\\" example.
\\n\\n" } Possible action objects in the JSON array: - {"action": "create_update", "filename": "path/to/file.ext", "content": "file content string here..."} - {"action": "delete", "filename": "path/to/file.ext"} - {"action": "chat", "content": "Your helpful answer string here..."} **VERY IMPORTANT - UPDATING FILES:** If the user asks you to modify an existing file (e.g., "add a footer to index.html", "change the button color in style.css"), you MUST provide the **ENTIRE**, complete, updated file content within the 'content' field of the 'create_update' action object, following all JSON formatting rules. Do NOT provide only the changed lines or a diff. **REACT PREVIEWS:** If the user asks for a simple React component/app to preview, generate a SINGLE self-contained HTML file (e.g., 'react_preview.html') using 'create_update'. This file MUST use CDN links for React/ReactDOM/Babel, have a