ada-web

Ada AI is a specialized coding assistant designed for developers and students. Powered by Google Gemini 3.1, it provides real-time, streamin...

git clone https://github.com/notamitgamer/ada-web.git
· rebuilt hourlyOpen on GitHub
commit a58bc4862be9271441b6b0e12f50be7df40dae7d
parent f2fe393db50288b6a488f380f76d374ae1ab710b
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sun, 18 Jan 2026 07:04:06 +0000

Add backend endpoints for profile and chat management

Co-authored-by: notamitgamer <199483577+notamitgamer@users.noreply.github.com>

Diffstat:
Mbackend.py | 244++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Aindex.html.backup | 639+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 882 insertions(+), 1 deletion(-)

diff --git a/backend.py b/backend.py @@ -164,6 +164,230 @@ def generate_title(): print(f"❌ Error generating title: {type(e).__name__}: {e}") return jsonify({"title": "New Chat"}) +@app.route('/api/profile', methods=['GET']) +def get_profile(): + """Get user profile from Firestore.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + user_ref = db.collection('users').document(user_uid) + doc = user_ref.get() + + if doc.exists: + return jsonify(doc.to_dict()) + else: + # Create default profile + default_profile = { + "uid": user_uid, + "displayName": "", + "email": "", + "photoURL": "", + "age": "", + "location": "", + "bio": "", + "totalChats": 0, + "totalMessages": 0, + "totalCodeSnippets": 0, + "theme": "dark", + "codeTheme": "dracula", + "fontSize": 13, + "createdAt": datetime.datetime.now(datetime.timezone.utc) + } + user_ref.set(default_profile) + return jsonify(default_profile) + except Exception as e: + print(f"❌ Error getting profile: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to get profile"}), 500 + +@app.route('/api/profile', methods=['PUT']) +def update_profile(): + """Update user profile in Firestore.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + data = request.json + allowed_fields = ['displayName', 'age', 'location', 'bio', 'photoURL', 'theme', 'codeTheme', 'fontSize'] + update_data = {k: v for k, v in data.items() if k in allowed_fields} + update_data['updatedAt'] = datetime.datetime.now(datetime.timezone.utc) + + user_ref = db.collection('users').document(user_uid) + user_ref.update(update_data) + + return jsonify({"success": True, "message": "Profile updated"}) + except Exception as e: + print(f"❌ Error updating profile: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to update profile"}), 500 + +@app.route('/api/chats', methods=['GET']) +def get_chats(): + """Get all user's chat sessions from Firestore.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + chats_ref = db.collection('users').document(user_uid).collection('chats') + docs = chats_ref.order_by('updatedAt', direction=firestore.Query.DESCENDING).stream() + + chats = [] + for doc in docs: + chat_data = doc.to_dict() + chat_data['id'] = doc.id + # Only send metadata, not full messages + chats.append({ + 'id': chat_data.get('id'), + 'title': chat_data.get('title', 'New Chat'), + 'createdAt': chat_data.get('createdAt'), + 'updatedAt': chat_data.get('updatedAt'), + 'isPinned': chat_data.get('isPinned', False), + 'messageCount': len(chat_data.get('messages', [])) + }) + + return jsonify({"chats": chats}) + except Exception as e: + print(f"❌ Error getting chats: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to get chats"}), 500 + +@app.route('/api/chats/<chat_id>', methods=['GET']) +def get_chat(chat_id): + """Get specific chat session.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + chat_ref = db.collection('users').document(user_uid).collection('chats').document(chat_id) + doc = chat_ref.get() + + if not doc.exists: + return jsonify({"error": "Chat not found"}), 404 + + chat_data = doc.to_dict() + chat_data['id'] = doc.id + return jsonify(chat_data) + except Exception as e: + print(f"❌ Error getting chat: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to get chat"}), 500 + +@app.route('/api/chats/<chat_id>', methods=['DELETE']) +def delete_chat(chat_id): + """Delete a chat session.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + chat_ref = db.collection('users').document(user_uid).collection('chats').document(chat_id) + chat_ref.delete() + return jsonify({"success": True, "message": "Chat deleted"}) + except Exception as e: + print(f"❌ Error deleting chat: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to delete chat"}), 500 + +@app.route('/api/chats/<chat_id>/rename', methods=['PUT']) +def rename_chat(chat_id): + """Rename a chat session.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + data = request.json + new_title = data.get('title', 'New Chat') + + chat_ref = db.collection('users').document(user_uid).collection('chats').document(chat_id) + chat_ref.update({ + 'title': new_title, + 'updatedAt': datetime.datetime.now(datetime.timezone.utc) + }) + + return jsonify({"success": True, "message": "Chat renamed"}) + except Exception as e: + print(f"❌ Error renaming chat: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to rename chat"}), 500 + +@app.route('/api/chats/<chat_id>/export', methods=['GET']) +def export_chat(chat_id): + """Export chat as JSON.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + chat_ref = db.collection('users').document(user_uid).collection('chats').document(chat_id) + doc = chat_ref.get() + + if not doc.exists: + return jsonify({"error": "Chat not found"}), 404 + + chat_data = doc.to_dict() + chat_data['id'] = doc.id + + # Convert timestamps to ISO format for JSON serialization + if 'createdAt' in chat_data: + chat_data['createdAt'] = chat_data['createdAt'].isoformat() if hasattr(chat_data['createdAt'], 'isoformat') else str(chat_data['createdAt']) + if 'updatedAt' in chat_data: + chat_data['updatedAt'] = chat_data['updatedAt'].isoformat() if hasattr(chat_data['updatedAt'], 'isoformat') else str(chat_data['updatedAt']) + + for msg in chat_data.get('messages', []): + if 'timestamp' in msg: + msg['timestamp'] = msg['timestamp'].isoformat() if hasattr(msg['timestamp'], 'isoformat') else str(msg['timestamp']) + + return jsonify(chat_data) + except Exception as e: + print(f"❌ Error exporting chat: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to export chat"}), 500 + +@app.route('/api/chats/<chat_id>/pin', methods=['PUT']) +def pin_chat(chat_id): + """Toggle pin status of a chat.""" + user_uid = verify_token(request.headers.get('Authorization')) + if not user_uid: + return jsonify({"error": "Unauthorized"}), 401 + + if not db: + return jsonify({"error": "Database unavailable"}), 503 + + try: + data = request.json + is_pinned = data.get('isPinned', False) + + chat_ref = db.collection('users').document(user_uid).collection('chats').document(chat_id) + chat_ref.update({ + 'isPinned': is_pinned, + 'updatedAt': datetime.datetime.now(datetime.timezone.utc) + }) + + return jsonify({"success": True, "isPinned": is_pinned}) + except Exception as e: + print(f"❌ Error pinning chat: {type(e).__name__}: {e}") + return jsonify({"error": "Failed to pin chat"}), 500 + @app.route('/api/chat', methods=['POST']) def chat(): """ @@ -197,7 +421,13 @@ def chat(): if code_ctx: context_str += f"\n\n[CURRENT EDITOR CONTENT]:\n{code_ctx}\n" if file_ctx: - context_str += f"\n\n[UPLOADED FILE CONTENT]:\n{file_ctx}\n" + # Handle both string and object formats + if isinstance(file_ctx, dict): + file_name = file_ctx.get('name', 'uploaded_file') + file_content = file_ctx.get('content', '') + context_str += f"\n\n[UPLOADED FILE: {file_name}]:\n{file_content}\n" + else: + context_str += f"\n\n[UPLOADED FILE CONTENT]:\n{file_ctx}\n" # Add History (Gemini format) chat_history = [] @@ -250,9 +480,21 @@ def chat(): doc = doc_ref.get() if not doc.exists: + # First message - generate title + chat_title = "New Chat" + try: + title_model = genai.GenerativeModel("models/gemini-2.5-flash-preview-09-2025") + title_response = title_model.generate_content(f"Summarize this coding query into a 3-5 word title: '{user_msg}'") + chat_title = title_response.text.strip() + except Exception as title_err: + print(f"⚠️ Title generation failed: {title_err}") + doc_ref.set({ + "title": chat_title, "createdAt": datetime.datetime.now(datetime.timezone.utc), + "updatedAt": datetime.datetime.now(datetime.timezone.utc), "userId": user_uid, + "isPinned": False, "messages": [msg_data, ai_data] }) else: diff --git a/index.html.backup b/index.html.backup @@ -0,0 +1,638 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <!-- SEO & Verification --> + <meta name="google-site-verification" content="pwteHw44Ax3N2B1zkFzVJB7NIK0-xgQGRihK_vtZTF4" /> + <link rel="canonical" href="https://ada.amit.is-a.dev/" /> + <link rel="icon" type="image/x-icon" href="favicon-32x32.png" > + + <meta name="description" content="Ada Aranag - Your intelligent assistant for quick answers and engaging conversations. Specialized in Code Analysis and Generation."> + <meta name="keywords" content="Ada Aranag, Ada, AI, chatbot, assistant, intelligent, conversation, Ada AI, coding, programming, python, javascript, debugger"> + <meta name="author" content="Amit Dutta"> + + <!-- Open Graph / Facebook Meta Tags --> + <meta property="og:title" content="Ada // AI Coding Environment"> + <meta property="og:description" content="Your intelligent assistant for quick answers and engaging conversations. Specialized in Code Analysis and Generation."> + <meta property="og:image" content="https://ada.amit.is-a.dev/logo.png"> + <meta property="og:url" content="https://ada.amit.is-a.dev/"> + <meta property="og:type" content="website"> + <meta property="og:site_name" content="Ada Aranag"> + + <!-- Twitter Card Meta Tags --> + <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:title" content="Ada // AI Coding Environment"> + <meta name="twitter:description" content="Your intelligent assistant for quick answers and engaging conversations. Specialized in Code Analysis."> + <meta name="twitter:image" content="https://ada.amit.is-a.dev/logo.png"> + + <!-- Security Headers (Attempt to fix Popup Warning) --> + <meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin-allow-popups"> + + <!-- Schema.org JSON-LD --> + <script type="application/ld+json"> + { + "@context": "https://schema.org", + "@type": "Website", + "name": "Ada Aranag", + "alternateName":["Ada Aranag","Ada AI Chatbot","AI Assistant","Intelligent Chatbot","Ada"], + "url": "https://ada.amit.is-a.dev" + } + </script> + + <title>Ada // AI Coding Environment</title> + + <!-- Tailwind CSS --> + <script src="https://cdn.tailwindcss.com"></script> + + <!-- Fonts --> + <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet"> + + <!-- Libraries --> + <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js"></script> + + <!-- CodeMirror --> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/dracula.min.css"> + <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script> + + <style> + :root { + --bg-main: #050505; + --accent: #bef264; /* Lime 300 */ + --border: rgba(255,255,255,0.1); + --glass: rgba(20, 20, 20, 0.8); + } + body { background: var(--bg-main); color: #e5e5e5; font-family: 'Inter', sans-serif; overflow: hidden; } + .font-mono { font-family: 'JetBrains Mono', monospace; } + + /* Scrollbar */ + ::-webkit-scrollbar { width: 6px; height: 6px; } + ::-webkit-scrollbar-track { background: var(--bg-main); } + ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; } + ::-webkit-scrollbar-thumb:hover { background: var(--accent); } + + /* CodeMirror Overrides */ + .CodeMirror { height: 100% !important; font-family: 'JetBrains Mono', monospace; font-size: 13px; background: #0f0f10 !important; } + .cm-s-dracula.CodeMirror { background: #0f0f10 !important; } + + /* Markdown Styling */ + .prose p { margin-bottom: 0.8rem; line-height: 1.6; } + .prose h1, .prose h2, .prose h3 { color: var(--accent); font-weight: bold; margin-top: 1rem; margin-bottom: 0.5rem; } + .prose h1 { font-size: 1.5rem; } + .prose h2 { font-size: 1.25rem; } + .prose h3 { font-size: 1.1rem; } + .prose code { color: var(--accent); background: rgba(190, 242, 100, 0.1); padding: 2px 4px; border-radius: 4px; font-family: 'JetBrains Mono'; font-size: 0.85em; } + .prose pre { background: #111; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; border: 1px solid var(--border); margin-bottom: 1rem; } + .prose pre code { background: transparent; padding: 0; color: #ccc; } + .prose ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; } + .prose ol { list-style-type: decimal; padding-left: 1.5rem; margin-bottom: 1rem; } + .prose li { margin-bottom: 0.3rem; line-height: 1.5; } + .prose blockquote { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 1rem 0; color: #999; } + .prose a { color: var(--accent); text-decoration: underline; } + .prose strong { color: var(--accent); font-weight: 600; } + .prose em { font-style: italic; color: #aaa; } + + /* Utilities */ + .glass-panel { background: var(--glass); backdrop-filter: blur(12px); border-right: 1px solid var(--border); } + .loader { border: 2px solid #333; border-top: 2px solid var(--accent); border-radius: 50%; width: 16px; height: 16px; animation: spin 1s linear infinite; } + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + + /* Typing Indicator Animation */ + .typing-dot { + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 50%; + display: inline-block; + animation: typing 1.4s infinite; + opacity: 0.6; + } + @keyframes typing { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.6; } + 30% { transform: translateY(-10px); opacity: 1; } + } + + /* Mobile Tabs */ + .tab-btn.active { border-bottom: 2px solid var(--accent); color: var(--accent); } + </style> +</head> +<body class="h-screen w-screen flex flex-col md:flex-row relative"> + + <!-- LOGIN MODAL --> + <div id="auth-modal" class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center backdrop-blur-sm hidden"> + <div class="bg-neutral-900 border border-neutral-700 p-8 rounded-xl max-w-md w-full shadow-2xl relative overflow-hidden"> + <div class="absolute top-0 left-0 w-full h-1 bg-lime-300"></div> + <h2 class="text-2xl font-bold text-white mb-2 font-mono">System Login</h2> + <p class="text-neutral-400 mb-6 text-sm">Authenticate to access Ada Coding Environment.</p> + + <!-- Google Login --> + <button onclick="handleGoogleLogin()" class="w-full flex items-center justify-center gap-3 bg-white text-black py-3 rounded font-bold hover:bg-gray-200 transition-colors mb-6"> + <svg class="w-5 h-5" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.84z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg> + Sign in with Google + </button> + + <!-- Divider --> + <div class="relative mb-6"> + <div class="absolute inset-0 flex items-center"><div class="w-full border-t border-white/10"></div></div> + <div class="relative flex justify-center text-xs uppercase"><span class="bg-neutral-900 px-2 text-neutral-500">Or continue with email</span></div> + </div> + + <!-- Email/Pass Inputs --> + <div class="space-y-4 mb-4"> + <input type="email" id="email-input" placeholder="Email Address" class="w-full bg-neutral-800 border border-neutral-700 rounded p-3 text-white focus:border-lime-300 outline-none transition-colors placeholder-neutral-500 font-mono text-sm"> + <input type="password" id="password-input" placeholder="Password" class="w-full bg-neutral-800 border border-neutral-700 rounded p-3 text-white focus:border-lime-300 outline-none transition-colors placeholder-neutral-500 font-mono text-sm"> + </div> + + <!-- Email Buttons --> + <div class="flex gap-3"> + <button onclick="handleEmailLogin()" class="flex-1 bg-lime-300 text-black py-3 rounded font-bold hover:bg-lime-400 transition-colors text-sm font-mono uppercase">Login</button> + <button onclick="handleEmailSignUp()" class="flex-1 bg-transparent border border-neutral-600 text-white py-3 rounded font-bold hover:border-lime-300 hover:text-lime-300 transition-colors text-sm font-mono uppercase">Sign Up</button> + </div> + + <div class="text-center text-xs text-neutral-500 font-mono mt-6">SECURE CONNECTION // FIREBASE AUTH</div> + </div> + </div> + + <!-- MAIN UI --> + + <!-- LEFT PANE: SIDEBAR + CHAT --> + <div id="left-pane" class="w-full md:w-[40%] flex flex-col h-full border-r border-white/10 relative bg-neutral-900/50"> + + <!-- HEADER --> + <div class="h-14 border-b border-white/10 flex items-center justify-between px-4 bg-neutral-900"> + <div class="flex items-center gap-2"> + <div class="w-2 h-2 bg-lime-300 rounded-full animate-pulse"></div> + <span class="font-mono font-bold text-lime-300 tracking-wider">ADA // AI</span> + </div> + <div class="flex items-center gap-3"> + <a href="docs.html" class="hidden md:block text-xs text-neutral-400 hover:text-lime-300 transition-colors font-mono" title="Documentation"> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg> + </a> + <button onclick="toggleSidebar()" class="md:hidden text-neutral-400 hover:text-white"> + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg> + </button> + <div id="user-profile" class="hidden md:flex items-center gap-2 cursor-pointer" onclick="signOutUser()"> + <img id="user-avatar" src="" class="w-6 h-6 rounded-full border border-lime-300/30"> + </div> + </div> + </div> + + <!-- HISTORY SIDEBAR (Off-canvas on mobile) --> + <div id="sidebar" class="absolute inset-y-0 left-0 w-64 bg-black border-r border-white/10 transform -translate-x-full transition-transform duration-300 z-40 md:relative md:translate-x-0 md:w-0 md:border-none md:hidden overflow-hidden"> + <!-- Populated via JS --> + </div> + + <!-- CHAT AREA --> + <div id="chat-container" class="flex-1 overflow-y-auto p-4 space-y-6 relative"> + <!-- Welcome Message --> + <div class="flex gap-4"> + <div class="w-8 h-8 rounded bg-lime-300/10 flex items-center justify-center border border-lime-300/20 text-lime-300 text-xs font-mono">AI</div> + <div class="flex-1"> + <div class="text-sm text-lime-300 font-mono mb-1">Ada</div> + <div class="text-sm text-neutral-300 leading-relaxed prose"> + <p>Systems Online. Ready for code analysis.</p> + <p>Paste code, upload a file, or ask a question.</p> + </div> + </div> + </div> + </div> + + <!-- FILE UPLOAD PREVIEW --> + <div id="file-preview" class="hidden px-4 py-2 bg-neutral-800 border-t border-white/5 flex items-center justify-between"> + <div class="flex items-center gap-2 text-xs font-mono text-lime-300"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> + <span id="file-name">code.py</span> + </div> + <button onclick="clearFile()" class="text-neutral-500 hover:text-white">×</button> + </div> + + <!-- INPUT AREA --> + <div class="p-4 bg-neutral-900 border-t border-white/10"> + <div class="relative flex items-end gap-2 bg-neutral-800 p-2 rounded-lg border border-white/5 focus-within:border-lime-300/50 transition-colors"> + <input type="file" id="file-upload" class="hidden" onchange="handleFileUpload(this)"> + <button onclick="document.getElementById('file-upload').click()" class="p-2 text-neutral-400 hover:text-lime-300 transition-colors" title="Upload Context"> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg> + </button> + <textarea id="chat-input" rows="1" class="flex-1 bg-transparent text-sm text-white focus:outline-none resize-none py-2 font-mono max-h-32" placeholder="Ask about code..."></textarea> + <button onclick="sendMessage()" id="send-btn" class="p-2 bg-lime-300 text-black rounded hover:bg-white transition-colors"> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg> + </button> + </div> + </div> + </div> + + <!-- RIGHT PANE: CODE EDITOR --> + <div id="right-pane" class="hidden md:flex flex-1 flex-col h-full bg-[#0f0f10] relative"> + <!-- Editor Header --> + <div class="h-14 border-b border-white/10 flex items-center justify-between px-4 bg-[#0f0f10]"> + <div class="flex items-center gap-3"> + <div class="text-xs font-mono text-neutral-500 uppercase tracking-wider">Editor Canvas</div> + <div id="temp-code-badge" class="hidden px-2 py-0.5 bg-lime-300/10 border border-lime-300/30 text-lime-300 text-[10px] rounded font-mono">temp_code loaded</div> + </div> + <div class="flex items-center gap-3"> + <select id="lang-select" onchange="changeLanguage()" class="bg-neutral-800 text-xs text-white border border-neutral-700 rounded px-2 py-1 outline-none"> + <option value="python">Python</option> + <option value="javascript">JavaScript</option> + <option value="clike">C / C++ / Java</option> + </select> + <button onclick="runCode()" class="flex items-center gap-2 px-3 py-1.5 bg-neutral-800 hover:bg-lime-300 hover:text-black border border-neutral-700 rounded text-xs font-mono uppercase transition-colors"> + <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> + Run / Compile + </button> + </div> + </div> + + <!-- Editor Container --> + <div class="flex-1 relative overflow-hidden"> + <textarea id="code-editor"></textarea> + </div> + </div> + + <!-- MOBILE TABS --> + <div class="md:hidden fixed bottom-0 left-0 w-full bg-neutral-900 border-t border-white/10 flex z-30"> + <button onclick="switchTab('chat')" id="tab-chat" class="flex-1 py-3 text-center text-xs font-mono uppercase tab-btn active">Chat</button> + <button onclick="switchTab('code')" id="tab-code" class="flex-1 py-3 text-center text-xs font-mono uppercase tab-btn text-neutral-500">Code Canvas</button> + </div> + + <!-- JS LOGIC: Converted to Modules to prevent 'firebase is not defined' --> + <script type="module"> + import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js"; + import { getAuth, GoogleAuthProvider, signInWithPopup, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-auth.js"; + import { getFirestore } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore.js"; + + // --- CONFIG --- + const firebaseConfig = { + apiKey: "AIzaSyAhyoNYUSXWIxdBiTxwSDcU7HkhhLEVlOc", + authDomain: "ada-ai-aranag.firebaseapp.com", + projectId: "ada-ai-aranag", + storageBucket: "ada-ai-aranag.firebasestorage.app", + messagingSenderId: "865895114061", + appId: "1:865895114061:web:5fd4493b0e388727e6c304" + }; + + // --- INIT FIREBASE --- + const app = initializeApp(firebaseConfig); + const auth = getAuth(app); + const db = getFirestore(app); + + let currentUser = null; + let currentSessionId = null; + + // --- INIT EDITOR --- + // CodeMirror is global (loaded via script tags in head) + const editor = CodeMirror.fromTextArea(document.getElementById("code-editor"), { + lineNumbers: true, + mode: "python", + theme: "dracula", + lineWrapping: true, + indentUnit: 4 + }); + + window.addEventListener('resize', () => editor.refresh()); + + // Configure marked.js for better markdown rendering + marked.setOptions({ + breaks: true, + gfm: true, + headerIds: false, + mangle: false + }); + + // --- AUTH LOGIC --- + onAuthStateChanged(auth, (user) => { + if (user) { + currentUser = user; + document.getElementById('auth-modal').classList.add('hidden'); + document.getElementById('user-avatar').src = user.photoURL || 'https://ui-avatars.com/api/?name=User'; + initSession(); + loadHistory(); + } else { + document.getElementById('auth-modal').classList.remove('hidden'); + } + }); + + // --- EXPORTED FUNCTIONS (for HTML onclick) --- + window.handleGoogleLogin = function() { + const provider = new GoogleAuthProvider(); + signInWithPopup(auth, provider).catch(e => alert(e.message)); + } + + window.handleEmailLogin = function() { + const email = document.getElementById('email-input').value; + const pass = document.getElementById('password-input').value; + if(!email || !pass) return alert("Please enter email and password"); + signInWithEmailAndPassword(auth, email, pass).catch(e => alert(e.message)); + } + + window.handleEmailSignUp = function() { + const email = document.getElementById('email-input').value; + const pass = document.getElementById('password-input').value; + if(!email || !pass) return alert("Please enter email and password"); + createUserWithEmailAndPassword(auth, email, pass).catch(e => alert(e.message)); + } + + window.signOutUser = function() { + if(confirm("Sign out?")) signOut(auth); + } + + // --- SESSION & LZSTRING --- + function initSession() { + const urlParams = new URLSearchParams(window.location.search); + const lzCode = urlParams.get('code'); + if (lzCode) { + try { + const decoded = LZString.decompressFromEncodedURIComponent(lzCode); + editor.setValue(decoded); + document.getElementById('temp-code-badge').classList.remove('hidden'); + } catch (e) { console.error("LZString Decode Error", e); } + } + if (!currentSessionId) { + currentSessionId = crypto.randomUUID(); + } + } + + // --- FILE UPLOAD --- + let fileContextContent = ""; + + window.handleFileUpload = function(input) { + const file = input.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + fileContextContent = e.target.result; + document.getElementById('file-preview').classList.remove('hidden'); + document.getElementById('file-name').innerText = file.name; + + if(file.name.endsWith('.py')) changeLanguage('python'); + if(file.name.endsWith('.js')) changeLanguage('javascript'); + if(file.name.endsWith('.c') || file.name.endsWith('.cpp')) changeLanguage('clike'); + }; + reader.readAsText(file); + } + + window.clearFile = function() { + fileContextContent = ""; + document.getElementById('file-upload').value = ""; + document.getElementById('file-preview').classList.add('hidden'); + } + + // --- CHAT LOGIC --- + const chatContainer = document.getElementById('chat-container'); + const chatInput = document.getElementById('chat-input'); + let chatHistory = []; + + chatInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + window.sendMessage(); + } + }); + + window.sendMessage = async function() { + const text = chatInput.value.trim(); + if (!text && !fileContextContent) return; + if (!currentUser) { + alert("Please login first."); + return; + } + + appendMessage('user', text); + chatInput.value = ""; + chatInput.style.height = 'auto'; + + const codeContext = editor.getValue(); + // UI: AI Loading + const loadingId = appendLoading(); + + try { + // Get fresh token (force refresh to ensure it's valid) + const token = await currentUser.getIdToken(true); + + // Render Backend URL + const API_URL = 'https://ada-web.onrender.com/api/chat'; + + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + message: text, + history: chatHistory.slice(-10), + codeContext: codeContext, + fileContext: fileContextContent, + sessionId: currentSessionId + }) + }); + + if (!response.ok) { + if (response.status === 401) { + // Try to refresh token and retry once + const freshToken = await currentUser.getIdToken(true); + const retryResponse = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${freshToken}` + }, + body: JSON.stringify({ + message: text, + history: chatHistory.slice(-10), + codeContext: codeContext, + fileContext: fileContextContent, + sessionId: currentSessionId + }) + }); + + if (!retryResponse.ok) { + throw new Error("Authentication failed. Please sign out and sign in again."); + } + + // Process retry response + await processStreamingResponse(retryResponse, loadingId, text); + return; + } + + let errorMessage = `API Error: ${response.status}`; + try { + const errorData = await response.json(); + if (errorData.message) { + errorMessage = errorData.message; + } + } catch (e) { + // Could not parse error as JSON + } + throw new Error(errorMessage); + } + + await processStreamingResponse(response, loadingId, text); + + } catch (e) { + const loadingDiv = document.getElementById(loadingId); + loadingDiv.innerHTML = `<div class="text-sm text-red-400 bg-red-900/20 p-3 rounded-lg border border-red-500/30"> + <strong>Error:</strong> ${e.message} + </div>`; + console.error("Chat error:", e); + } + } + + async function processStreamingResponse(response, loadingId, userText) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let fullResponse = ""; + let aiMsgDiv = document.getElementById(loadingId); + + aiMsgDiv.innerHTML = '<div class="prose text-sm text-neutral-300 max-w-none"></div>'; + let contentDiv = aiMsgDiv.querySelector('.prose'); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + fullResponse += chunk; + + const parts = parseResponse(fullResponse); + contentDiv.innerHTML = marked.parse(parts.text); + + if (parts.code) { + if (editor.getValue().trim() !== parts.code.trim()) { + editor.setValue(parts.code); + } + } + chatContainer.scrollTop = chatContainer.scrollHeight; + } + + chatHistory.push({ user: userText, model: fullResponse }); + fileContextContent = ""; + document.getElementById('file-preview').classList.add('hidden'); + } + + function parseResponse(raw) { + const startMarker = "<<<CODE_START>>>"; + const endMarker = "<<<CODE_END>>>"; + let text = raw; + let code = null; + + if (raw.includes(startMarker)) { + const parts = raw.split(startMarker); + text = parts[0].trim(); + + if (parts[1]) { + if (parts[1].includes(endMarker)) { + // Complete code block + const codeParts = parts[1].split(endMarker); + code = codeParts[0].trim(); + // Append any text after code block back to main text + if (codeParts[1] && codeParts[1].trim()) { + text += "\n\n" + codeParts[1].trim(); + } + } else { + // Incomplete code block (still streaming) + code = parts[1].trim(); + } + } + } + + return { text, code }; + } + + function appendMessage(role, text) { + const div = document.createElement('div'); + div.className = "flex gap-4 animate-fade-in"; + if (role === 'user') { + div.innerHTML = ` + <img src="${currentUser.photoURL}" class="w-8 h-8 rounded-full border border-white/10"> + <div class="flex-1"> + <div class="text-sm text-neutral-500 font-mono mb-1">You</div> + <div class="text-sm text-white bg-neutral-800 p-3 rounded-lg border border-white/5 inline-block">${text}</div> + </div> + `; + } + chatContainer.appendChild(div); + chatContainer.scrollTop = chatContainer.scrollHeight; + } + + function appendLoading() { + const id = 'msg-' + Date.now(); + const div = document.createElement('div'); + div.className = "flex gap-4"; + div.innerHTML = ` + <div class="w-8 h-8 rounded bg-lime-300/10 flex items-center justify-center border border-lime-300/20 text-lime-300 text-xs font-mono">AI</div> + <div class="flex-1" id="${id}"> + <div class="flex gap-1 mt-2"> + <div class="typing-dot"></div><div class="typing-dot" style="animation-delay:0.2s"></div><div class="typing-dot" style="animation-delay:0.4s"></div> + </div> + </div> + `; + chatContainer.appendChild(div); + chatContainer.scrollTop = chatContainer.scrollHeight; + return id; + } + + // --- EDITOR ACTIONS (Exported to Window) --- + window.changeLanguage = function(val) { + const lang = val || document.getElementById('lang-select').value; + editor.setOption("mode", lang); + document.getElementById('lang-select').value = lang; + } + + window.runCode = function() { + const code = editor.getValue(); + const lang = document.getElementById('lang-select').value; + const compressed = LZString.compressToEncodedURIComponent(code); + let urlLang = 'python'; + if (lang === 'clike') urlLang = 'cpp'; + if (lang === 'javascript') urlLang = 'nodejs'; + window.open(`https://compiler.amit.is-a.dev/${urlLang}?code=${compressed}`, '_blank'); + } + + window.switchTab = function(tab) { + const left = document.getElementById('left-pane'); + const right = document.getElementById('right-pane'); + const tabChat = document.getElementById('tab-chat'); + const tabCode = document.getElementById('tab-code'); + if (tab === 'chat') { + left.classList.remove('hidden'); + right.classList.add('hidden'); + tabChat.classList.add('active', 'text-black', 'border-lime-300'); + tabChat.classList.remove('text-neutral-500'); + tabCode.classList.remove('active'); + tabCode.classList.add('text-neutral-500'); + } else { + left.classList.add('hidden'); + right.classList.remove('hidden'); + right.classList.add('flex'); + editor.refresh(); + tabCode.classList.add('active'); + tabCode.classList.remove('text-neutral-500'); + tabChat.classList.remove('active'); + tabChat.classList.add('text-neutral-500'); + } + } + + window.toggleSidebar = function() { + const sb = document.getElementById('sidebar'); + if (sb.classList.contains('-translate-x-full')) { + sb.classList.remove('-translate-x-full'); + } else { + sb.classList.add('-translate-x-full'); + } + } + + async function loadHistory() { + // Placeholder + } + + </script> +</body> +</html> +\ No newline at end of file