Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <title>CODE VED | Engineered by Divy Patel</title> | |
| <!-- Premium Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <!-- Syntax Highlighting & Parsers --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script> | |
| <style> | |
| :root { | |
| /* 🎨 SUBTLE & CLEAN COLOR PALETTE */ | |
| --bg-main: #fcfdfd; | |
| --bg-sidebar: #f4f6f8; | |
| --bg-user-msg: #eff6ff; | |
| --bg-input: #ffffff; | |
| --text-primary: #0f172a; | |
| --text-secondary: #475569; | |
| --text-tertiary: #94a3b8; | |
| --border-light: #e2e8f0; | |
| --border-focus: #cbd5e1; | |
| --brand-color: #0f172a; | |
| --brand-accent: #3b82f6; | |
| --brand-success: #10b981; | |
| --brand-danger: #ef4444; | |
| --brand-warning: #eab308; /* Yellow */ | |
| --font-ui: 'Inter', sans-serif; | |
| --font-code: 'JetBrains Mono', monospace; | |
| --radius-sm: 8px; | |
| --radius-md: 14px; | |
| --radius-lg: 24px; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; height: 100dvh; width: 100vw; | |
| font-family: var(--font-ui); background-color: var(--bg-main); color: var(--text-primary); | |
| display: flex; overflow: hidden; -webkit-font-smoothing: antialiased; | |
| } | |
| button { background: none; border: none; cursor: pointer; color: inherit; font-family: inherit; } | |
| input, textarea { font-family: inherit; outline: none; border: none; background: transparent; } | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } | |
| /* --- SLEEK HAMBURGER MENU --- */ | |
| .btn-menu { | |
| position: absolute; top: 16px; left: 16px; z-index: 50; | |
| width: 40px; height: 40px; border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| background: transparent; color: var(--text-secondary); transition: 0.2s; | |
| } | |
| .btn-menu:hover { background: #e2e8f0; color: var(--text-primary); } | |
| /* --- SIDEBAR --- */ | |
| .sidebar { | |
| width: 280px; background-color: var(--bg-sidebar); border-right: 1px solid var(--border-light); | |
| display: flex; flex-direction: column; transition: transform 0.3s ease; z-index: 100; | |
| height: 100%; flex-shrink: 0; | |
| } | |
| .sidebar.collapsed { transform: translateX(-100%); position: absolute; } | |
| .sidebar-header { padding: 20px 20px 10px; display: flex; align-items: center; justify-content: space-between; } | |
| .sidebar-logo { font-weight: 600; font-size: 15px; letter-spacing: -0.3px; color: var(--text-primary); } | |
| .btn-new-chat { | |
| margin: 10px 16px; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 14px; | |
| border-radius: var(--radius-sm); background: #fff; border: 1px solid var(--border-light); | |
| font-size: 13px; font-weight: 500; transition: 0.2s; color: var(--text-primary); | |
| } | |
| .btn-new-chat:hover { border-color: var(--border-focus); box-shadow: 0 2px 4px rgba(0,0,0,0.02); } | |
| .sidebar-history { flex: 1; overflow-y: auto; padding: 10px 16px; display: flex; flex-direction: column; gap: 4px; } | |
| .history-header { display:flex; justify-content:space-between; align-items:center; padding: 0 4px 8px; margin-bottom:8px; border-bottom:1px solid var(--border-light); } | |
| .history-title-text { font-size:11px; font-weight:600; color:var(--text-tertiary); text-transform:uppercase; letter-spacing:0.5px; } | |
| .btn-clear-all { font-size:11px; font-weight:500; color:var(--text-tertiary); cursor:pointer; transition:0.2s; } | |
| .btn-clear-all:hover { color:var(--brand-danger); } | |
| .history-item { padding: 10px 12px; border-radius: var(--radius-sm); font-size: 13px; color: var(--text-secondary); cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: 0.2s; border: 1px solid transparent; } | |
| .history-item:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); } | |
| .history-item.active { background: #e2e8f0; color: var(--text-primary); font-weight: 500; border-color: var(--border-focus); } | |
| .history-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } | |
| .btn-delete-chat { background: none; border: none; color: var(--text-tertiary); cursor: pointer; padding: 4px; display: none; align-items: center; justify-content: center; border-radius: 4px; } | |
| .btn-delete-chat svg { width: 14px; height: 14px; transition: 0.2s; } | |
| .history-item:hover .btn-delete-chat { display: flex; } | |
| .btn-delete-chat:hover { color: var(--brand-danger); background: #fee2e2; } | |
| .sidebar-footer { padding: 16px; border-top: 1px solid var(--border-light); display: flex; align-items: center; gap: 12px; background: #fff; } | |
| .user-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--brand-accent); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; flex-shrink: 0; box-shadow: 0 2px 5px rgba(59, 130, 246, 0.3); } | |
| .user-info-box { flex: 1; overflow: hidden; display: flex; flex-direction: column; justify-content: center; } | |
| .user-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-primary); line-height: 1.2; } | |
| .user-sub { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; } | |
| .btn-power { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); transition: 0.2s; } | |
| .btn-power:hover { background: #fee2e2; color: var(--brand-danger); } | |
| /* --- MAIN WORKSPACE AREA --- */ | |
| .main-area { flex: 1; display: flex; flex-direction: column; position: relative; transition: 0.3s; height: 100%; overflow: hidden; } | |
| /* --- CHAT CONTAINER --- */ | |
| .chat-container { flex: 1; overflow-y: auto; padding: 60px 20px 140px; display: flex; flex-direction: column; align-items: center; scroll-behavior: smooth; } | |
| .welcome-center { margin: auto; text-align: center; display: flex; flex-direction: column; align-items: center; animation: fadeIn 0.4s ease; width: 100%; justify-content: center; flex: 1; } | |
| .welcome-center img { width: 68px; height: 68px; border-radius: 18px; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); object-fit: cover; } | |
| .welcome-center h1 { font-size: 24px; font-weight: 500; color: var(--text-primary); margin-bottom: 6px; letter-spacing: -0.5px; } | |
| .welcome-center p { font-size: 14px; color: var(--text-secondary); } | |
| #chatMessages { width: 100%; display: flex; flex-direction: column; align-items: center; } | |
| .message-wrapper { width: 100%; max-width: 780px; margin-bottom: 32px; display: flex; flex-direction: column; } | |
| .user-message { align-self: flex-end; max-width: 85%; background: var(--bg-user-msg); padding: 14px 18px; border-radius: 20px 20px 6px 20px; font-size: 15px; line-height: 1.5; color: var(--text-primary); border: 1px solid #dbeafe; } | |
| .bot-message { align-self: flex-start; max-width: 100%; display: flex; gap: 16px; width: 100%; } | |
| .bot-avatar { width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0; overflow: hidden; border: 1px solid var(--border-light); background: #fff; } | |
| .bot-avatar img { width: 100%; height: 100%; object-fit: cover; } | |
| .bot-content { flex: 1; font-size: 15px; line-height: 1.65; color: var(--text-primary); min-width: 0; padding-top: 2px; } | |
| .bot-content p { margin-bottom: 16px; } | |
| .bot-content p:last-child { margin-bottom: 0; } | |
| .bot-content strong { font-weight: 600; color: #000; } | |
| .bot-content code { font-family: var(--font-code); background: var(--bg-sidebar); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; color: #0ea5e9; } | |
| .bot-content pre { background: #ffffff; padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-light); overflow-x: auto; margin: 16px 0; box-shadow: 0 1px 2px rgba(0,0,0,0.02); } | |
| .bot-content pre code { background: none; padding: 0; color: inherit; font-size: 13.5px; } | |
| .chat-attachment { display: inline-flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--border-light); padding: 6px 12px; border-radius: var(--radius-md); font-size: 13px; font-weight: 500; margin-bottom: 10px; } | |
| .chat-img-preview { max-width: 240px; border-radius: var(--radius-md); border: 1px solid var(--border-light); margin-bottom: 10px; } | |
| /* --- VISIBLE THINKING DROPDOWN --- */ | |
| .qwen-think-box { | |
| background: #ffffff; border: 1px solid var(--border-light); border-radius: 12px; | |
| margin-bottom: 16px; overflow: hidden; transition: 0.2s; | |
| } | |
| .qwen-think-box summary { | |
| padding: 10px 14px; font-size: 13px; font-weight: 500; color: var(--text-secondary); | |
| cursor: pointer; user-select: none; list-style: none; display: flex; align-items: center; gap: 8px; | |
| background: var(--bg-sidebar); | |
| } | |
| .qwen-think-box summary::-webkit-details-marker { display: none; } | |
| .qwen-think-box summary svg.arrow { width: 14px; height: 14px; transition: transform 0.2s; } | |
| .qwen-think-box[open] summary svg.arrow { transform: rotate(90deg); } | |
| .qwen-think-content { padding: 14px; border-top: 1px solid var(--border-light); font-size: 13.5px; color: var(--text-secondary); font-style: italic; background: #fff; line-height: 1.6; } | |
| /* --- ✨ SEARCH & LOCATION ANIMATIONS (NO SPINNING, ONLY FLICKER) --- */ | |
| .action-status { | |
| display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; | |
| background: var(--bg-sidebar); border-radius: 12px; font-size: 13px; font-weight: 500; | |
| color: var(--text-secondary); margin-bottom: 16px; border: 1px solid var(--border-light); | |
| cursor: default; user-select: none; | |
| } | |
| .action-status.active { background: #eff6ff; border-color: #bfdbfe; color: var(--brand-accent); } | |
| .flicker-text { animation: pulseOpacity 1.2s infinite alternate; } | |
| @keyframes pulseOpacity { | |
| 0% { opacity: 0.3; } | |
| 100% { opacity: 1; } | |
| } | |
| /* --- ACTION CONTROLS --- */ | |
| .msg-actions { display: flex; gap: 6px; margin-top: 12px; opacity: 0; transition: opacity 0.2s; align-items: center; } | |
| .message-wrapper:hover .msg-actions { opacity: 1; } | |
| @media (hover: none) { .msg-actions { opacity: 1; } } | |
| .action-btn { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 500; color: var(--text-tertiary); padding: 4px 8px; border-radius: 6px; transition: 0.2s; } | |
| .action-btn:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); } | |
| .action-btn.stop-btn:hover { background: #fee2e2; color: var(--brand-danger); } | |
| /* --- APPLE GLASS LIQUID MORPHISM INPUT DOCK --- */ | |
| .input-dock { | |
| position: absolute; bottom: 0; left: 0; width: 100%; | |
| padding: 15px 20px; padding-bottom: calc(20px + env(safe-area-inset-bottom)); | |
| background: rgba(252, 253, 253, 0.7); | |
| backdrop-filter: blur(20px) saturate(150%); | |
| -webkit-backdrop-filter: blur(20px) saturate(150%); | |
| border-top: 1px solid rgba(0,0,0,0.04); | |
| display: flex; flex-direction: column; align-items: center; z-index: 50; | |
| } | |
| .loc-toast { | |
| display: none; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; | |
| color: var(--text-secondary); margin-bottom: 8px; background: rgba(255,255,255,0.8); | |
| padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border-light); | |
| } | |
| .input-container { | |
| width: 100%; max-width: 780px; | |
| background: rgba(255, 255, 255, 0.95); | |
| border: 1px solid var(--border-focus); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.03); | |
| border-radius: var(--radius-lg); padding: 8px 12px; | |
| display: flex; flex-direction: column; transition: 0.2s; | |
| } | |
| .input-container:focus-within { border-color: var(--brand-accent); box-shadow: 0 4px 25px rgba(59, 130, 246, 0.08); } | |
| .file-preview-bar { display: none; padding: 4px 8px 12px; align-items: center; gap: 12px; border-bottom: 1px solid var(--border-light); margin-bottom: 8px; } | |
| .file-preview-bar.active { display: flex; } | |
| .file-preview-icon { width: 36px; height: 36px; border-radius: 8px; object-fit: cover; background: var(--bg-sidebar); border: 1px solid var(--border-light); display: flex; align-items: center; justify-content: center; } | |
| .file-preview-name { flex: 1; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-primary); } | |
| .btn-remove-file { width: 24px; height: 24px; border-radius: 50%; background: #fee2e2; color: var(--brand-danger); display: flex; align-items: center; justify-content: center; } | |
| .input-row { display: flex; align-items: flex-end; gap: 8px; width: 100%; } | |
| .tools-left { display: flex; gap: 2px; align-items: center; } | |
| .tool-btn { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); transition: 0.2s; position: relative; flex-shrink: 0; } | |
| .tool-btn:hover { background: var(--bg-sidebar); color: var(--text-primary); } | |
| /* 🎨 DYNAMIC COLORS */ | |
| .tool-btn.active-env { color: var(--brand-accent); background: #eff6ff; } | |
| .tool-btn.active-think-low { color: var(--brand-danger); background: #fee2e2; } | |
| .tool-btn.active-think-medium { color: var(--brand-warning); background: #fef3c7; } | |
| .tool-btn.active-think-high { color: var(--brand-accent); background: #eff6ff; } | |
| .tool-btn.recording { color: var(--brand-danger); animation: pulseOpacity 1s infinite alternate; } | |
| .chat-input { flex: 1; padding: 8px 4px; font-size: 15px; resize: none; max-height: 120px; min-height: 24px; color: var(--text-primary); line-height: 1.5; background: transparent; } | |
| .chat-input::placeholder { color: var(--text-tertiary); } | |
| .btn-send { width: 36px; height: 36px; border-radius: 50%; background: var(--brand-color); color: #fff; display: flex; align-items: center; justify-content: center; transition: 0.2s; flex-shrink: 0; } | |
| .btn-send:disabled { background: #cbd5e1; cursor: not-allowed; } | |
| /* --- AUTH MODAL --- */ | |
| .auth-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(4px); display: none; align-items: center; justify-content: center; z-index: 2000; } | |
| .auth-modal { background: #fff; width: 90%; max-width: 380px; border-radius: var(--radius-md); padding: 32px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); position: relative; } | |
| .auth-close { position: absolute; top: 16px; right: 16px; color: var(--text-tertiary); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: 0.2s; } | |
| .auth-close:hover { background: var(--bg-sidebar); color: var(--text-primary); } | |
| .auth-title { font-size: 20px; font-weight: 600; margin-bottom: 24px; text-align: center; color: var(--text-primary); } | |
| .auth-tabs { display: flex; background: var(--bg-sidebar); border-radius: var(--radius-sm); padding: 4px; margin-bottom: 20px; } | |
| .auth-tab { flex: 1; text-align: center; padding: 8px; font-size: 13px; font-weight: 500; cursor: pointer; border-radius: 6px; color: var(--text-secondary); transition: 0.2s; } | |
| .auth-tab.active { background: #fff; color: var(--text-primary); box-shadow: 0 1px 2px rgba(0,0,0,0.05); } | |
| .auth-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border-light); border-radius: var(--radius-sm); font-size: 14px; margin-bottom: 12px; transition: 0.2s; background: #fff; } | |
| .auth-input:focus { border-color: var(--brand-accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } | |
| .auth-btn { width: 100%; padding: 12px; background: var(--brand-color); color: #fff; font-size: 14px; font-weight: 500; border-radius: var(--radius-sm); transition: 0.2s; } | |
| .auth-phase { display: none; } | |
| .auth-phase.active { display: block; } | |
| .auth-message { font-size: 12px; text-align: center; margin-top: 12px; min-height: 18px; font-weight: 500; } | |
| /* --- CANVAS / WORKSPACE --- */ | |
| .workspace-panel { width: 0; background: #fff; border-left: 1px solid var(--border-light); display: flex; flex-direction: column; transition: width 0.3s ease; overflow: hidden; z-index: 90; position: relative; } | |
| .workspace-panel.active { width: 50%; box-shadow: -4px 0 24px rgba(0,0,0,0.02); } | |
| .ws-header { padding: 12px 20px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; background: #fff; } | |
| .ws-tabs { display: flex; gap: 4px; background: var(--bg-sidebar); padding: 4px; border-radius: var(--radius-sm); } | |
| .ws-tab { padding: 6px 14px; font-size: 12px; font-weight: 500; border-radius: 6px; cursor: pointer; color: var(--text-secondary); transition: 0.2s; } | |
| .ws-tab.active { background: #fff; color: var(--text-primary); box-shadow: 0 1px 2px rgba(0,0,0,0.05); } | |
| .ws-actions { display: flex; align-items: center; gap: 4px; } | |
| .ws-btn { padding: 6px 10px; border-radius: 6px; color: var(--text-secondary); transition: 0.2s; font-size: 12px; display: flex; align-items: center; gap: 6px; } | |
| .ws-btn:hover { background: var(--bg-sidebar); color: var(--text-primary); } | |
| .ws-content { flex: 1; overflow: hidden; position: relative; } | |
| .ws-code { height: 100%; overflow: auto; padding: 20px; background: #ffffff; } | |
| .ws-code pre { margin: 0; font-family: var(--font-code); font-size: 13.5px; } | |
| .ws-preview { display: none; height: 100%; width: 100%; flex-direction: column; background: #fff; } | |
| .ws-console { background: #0f172a; color: #f8fafc; font-family: var(--font-code); padding: 20px; font-size: 13px; flex: 1; overflow: auto; white-space: pre-wrap; line-height: 1.5; } | |
| /* Dropdowns */ | |
| .hidden-input { display: none; } | |
| .dropdown-menu { position: absolute; bottom: calc(100% + 10px); left: 0; background: #fff; border: 1px solid var(--border-light); border-radius: var(--radius-md); padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); display: none; flex-direction: column; min-width: 160px; z-index: 100; } | |
| .dropdown-menu.active { display: flex; animation: fadeIn 0.2s; } | |
| .dropdown-item { padding: 10px 12px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; color: var(--text-secondary); } | |
| .dropdown-item:hover { background: var(--bg-sidebar); color: var(--text-primary); } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| .typing-indicator { display: flex; gap: 4px; padding: 6px 0; } | |
| .typing-dot { width: 5px; height: 5px; background: var(--text-tertiary); border-radius: 50%; animation: typeBounce 1.4s infinite ease-in-out both; } | |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } .typing-dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typeBounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } | |
| svg { width: 100%; height: 100%; } | |
| .mobile-overlay { display: none; position: fixed; inset: 0; background: rgba(15,23,42,0.2); backdrop-filter: blur(2px); z-index: 95; opacity: 0; transition: 0.3s; pointer-events: none; } | |
| .mobile-overlay.active { display: block; opacity: 1; pointer-events: all; } | |
| @media (max-width: 900px) { | |
| .sidebar { position: fixed; left: 0; top: 0; height: 100%; box-shadow: 4px 0 24px rgba(0,0,0,0.05); } | |
| .sidebar.collapsed { transform: translateX(-100%); box-shadow: none; } | |
| .workspace-panel.active { width: 100%; position: absolute; top:0; left:0; height:100%; z-index: 1000; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <button class="btn-menu" id="btnMenuOpen" onclick="UI.toggleSidebar()" title="Menu"> | |
| <svg style="width:24px;height:24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="14" y2="15"></line></svg> | |
| </button> | |
| <div class="mobile-overlay" id="mobileOverlay" onclick="UI.toggleSidebar()"></div> | |
| <aside class="sidebar collapsed" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="sidebar-logo">CODE VED</div> | |
| <button class="btn-menu" style="position:static; width:32px; height:32px;" onclick="UI.toggleSidebar()"> | |
| <svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> | |
| </button> | |
| </div> | |
| <button class="btn-new-chat" onclick="HistoryManager.startNew()"> | |
| <svg style="width:14px; height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> | |
| New Workspace | |
| </button> | |
| <div class="sidebar-history" id="chatHistory"></div> | |
| <div class="sidebar-footer"> | |
| <div class="user-avatar" id="uAv">G</div> | |
| <div class="user-info-box"> | |
| <div class="user-name" id="uName">Guest Session</div> | |
| <div class="user-sub" id="uSub" style="cursor:pointer; color:var(--brand-accent);" onclick="Auth.openModal()">Secure Login</div> | |
| </div> | |
| <button class="btn-power" id="btnLogout" onclick="Auth.handleLogout()" style="display:none;" title="Logout"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg> | |
| </button> | |
| </div> | |
| </aside> | |
| <main class="main-area"> | |
| <div class="chat-container" id="chatContainer"> | |
| <!-- 🌟 STATIC WELCOME SCREEN --> | |
| <div class="welcome-center" id="welcomeScreen"> | |
| <img src="https://i.ibb.co/MyYStcGP/TIRANGA-20260613-131924-0000.png" alt="CODE VED Logo"> | |
| <h1>CODE VED</h1> | |
| <p id="welcomeGreeting">Engineered by Divy Patel</p> | |
| </div> | |
| <!-- 💬 ALL MESSAGES GO HERE --> | |
| <div id="chatMessages"></div> | |
| </div> | |
| <div class="input-dock" id="inputDock"> | |
| <!-- 🌍 Environment Status Toast --> | |
| <div class="loc-toast" id="locStatus"></div> | |
| <div class="input-container"> | |
| <div class="file-preview-bar" id="filePreviewBar"> | |
| <img id="imgPreview" class="file-preview-icon" src="" style="display:none;"> | |
| <div id="docPreview" class="file-preview-icon" style="display:none;"> | |
| <svg style="width:16px;height:16px; color:var(--text-secondary);" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg> | |
| </div> | |
| <div class="file-preview-name" id="fileName">document.pdf</div> | |
| <button class="btn-remove-file" onclick="FileSys.discard()"><svg style="width:12px;height:12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button> | |
| </div> | |
| <div class="input-row"> | |
| <div class="tools-left"> | |
| <!-- File Upload --> | |
| <div style="position: relative;"> | |
| <button class="tool-btn" onclick="UI.toggleAttachMenu()" title="Attach File"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg> | |
| </button> | |
| <div class="dropdown-menu" id="attachMenu"> | |
| <div class="dropdown-item" onclick="document.getElementById('imgUpload').click()"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg> Upload Image</div> | |
| <div class="dropdown-item" onclick="document.getElementById('docUpload').click()"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg> Upload Document</div> | |
| </div> | |
| <input type="file" id="imgUpload" class="hidden-input" accept="image/*" onchange="FileSys.process(this, 'image')"> | |
| <input type="file" id="docUpload" class="hidden-input" accept=".pdf,.txt,.docx,.html,.js,.py,.css,.cpp,.c,.json,.md" onchange="FileSys.process(this, 'document')"> | |
| </div> | |
| <!-- Voice Typing --> | |
| <button class="tool-btn" id="btnStt" onclick="Speech.toggle()" title="Voice Typing"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> | |
| </button> | |
| <!-- 🌍 UNIFIED ENVIRONMENT (Location + Weather) --> | |
| <button class="tool-btn" id="btnEnv" onclick="EnvironmentManager.toggle()" title="Environment (Location & Weather)"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> | |
| </button> | |
| <!-- Thinking Effort Menu --> | |
| <div style="position: relative;"> | |
| <button class="tool-btn" id="btnThink" onclick="ThinkingManager.toggleMenu()" title="Reasoning Effort"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a5 5 0 0 0-5 5v2a5 5 0 0 0 10 0V7a5 5 0 0 0-5-5z"></path><path d="M8 14H6a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2"></path></svg> | |
| </button> | |
| <div class="dropdown-menu" id="thinkMenu" style="min-width: 180px;"> | |
| <div class="dropdown-item" onclick="ThinkingManager.setEffort('low')">⚡ Low (Fast)</div> | |
| <div class="dropdown-item" onclick="ThinkingManager.setEffort('medium')">🧠 Medium (Balanced)</div> | |
| <div class="dropdown-item" onclick="ThinkingManager.setEffort('high')">🔍 High (Deep logic)</div> | |
| <div style="height:1px; background:var(--border-light); margin:4px 0;"></div> | |
| <div class="dropdown-item" style="color:var(--brand-danger);" onclick="ThinkingManager.disable()">❌ Disable Thinking</div> | |
| </div> | |
| </div> | |
| </div> | |
| <textarea class="chat-input" id="mainInput" placeholder="Message CODE VED..." rows="1"></textarea> | |
| <button class="btn-send" id="btnSend" onclick="Chat.handleSend()"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- WORKSPACE PANEL (RESTORED & FUNCTIONAL) --> | |
| <aside class="workspace-panel" id="workspacePanel"> | |
| <div class="ws-header"> | |
| <div class="ws-tabs"> | |
| <div class="ws-tab active" id="tabCode" onclick="Workspace.switchTab('code')">Code</div> | |
| <div class="ws-tab" id="tabPreview" onclick="Workspace.switchTab('preview')">Preview</div> | |
| </div> | |
| <div class="ws-actions"> | |
| <button class="ws-btn" onclick="Workspace.copy()" title="Copy Code"> | |
| <svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy | |
| </button> | |
| <button class="ws-btn" onclick="Workspace.close()" title="Close Panel"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="ws-content"> | |
| <div class="ws-code" id="wsCodeView"><pre><code id="wsCodeBlock" class="hljs"></code></pre></div> | |
| <div class="ws-preview" id="wsPreviewView"></div> | |
| </div> | |
| </aside> | |
| <!-- SECURE AUTHENTICATION MODAL --> | |
| <div class="auth-overlay" id="authModal"> | |
| <div class="auth-modal"> | |
| <button class="auth-close" onclick="Auth.closeModal()"><svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button> | |
| <div class="auth-title">System Access</div> | |
| <div class="auth-tabs"> | |
| <div class="auth-tab active" id="tabLogin" onclick="Auth.switchTab('login')">Login</div> | |
| <div class="auth-tab" id="tabRegister" onclick="Auth.switchTab('register')">Register</div> | |
| </div> | |
| <div id="flowLogin"> | |
| <div class="auth-phase active" id="loginPhase1"> | |
| <input type="email" class="auth-input" id="logEmail" placeholder="Email Address"> | |
| <button class="auth-btn" id="btnLogOtp" onclick="Auth.process('login_send_otp')">Continue</button> | |
| </div> | |
| <div class="auth-phase" id="loginPhase2"> | |
| <input type="number" class="auth-input" id="logOtp" placeholder="Enter OTP"> | |
| <button class="auth-btn" id="btnLogVerify" onclick="Auth.process('login_verify')">Verify & Access</button> | |
| <button style="margin-top:12px; font-size:12px; color:var(--text-secondary); width:100%; text-align:center; background:none; border:none; cursor:pointer;" onclick="Auth.switchPhase('loginPhase2', 'loginPhase1')">Back</button> | |
| </div> | |
| </div> | |
| <div id="flowRegister" style="display:none;"> | |
| <div class="auth-phase active" id="regPhase1"> | |
| <input type="text" class="auth-input" id="regName" placeholder="Full Name"> | |
| <input type="email" class="auth-input" id="regEmail" placeholder="Email Address"> | |
| <button class="auth-btn" id="btnRegOtp" onclick="Auth.process('register_send_otp')">Create Account</button> | |
| </div> | |
| <div class="auth-phase" id="regPhase2"> | |
| <input type="number" class="auth-input" id="regOtp" placeholder="Enter OTP"> | |
| <button class="auth-btn" id="btnRegVerify" onclick="Auth.process('register_verify')">Verify & Register</button> | |
| </div> | |
| </div> | |
| <div class="auth-message" id="authMsg"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- 1. CONFIG & STATE --- | |
| const Config = { | |
| GAS_URL: "https://script.google.com/macros/s/AKfycbzNO3inVc33ImhfLyde-JjjK9ZlPckLBksqCnCzelfhcklX6mp8KW8vfPTW4oWJTCcN/exec", | |
| API_ENDPOINT: "/api/chat", | |
| LOGO: "https://i.ibb.co/MyYStcGP/TIRANGA-20260613-131924-0000.png" | |
| }; | |
| const State = { | |
| user: localStorage.getItem('codeved_user') || null, | |
| name: localStorage.getItem('codeved_name') || null, | |
| guestCount: parseInt(localStorage.getItem('codeved_guest') || '0', 10), | |
| attachment: null, | |
| isProcessing: false, | |
| history: [], | |
| workspaces: [], | |
| currentWs: null, | |
| abortController: null, | |
| location: null, | |
| weatherContext: null, // Unified Environment Context | |
| thinkingMode: false, | |
| thinkingEffort: "medium", | |
| lastUserMessage: null, | |
| currentThreadId: null, | |
| currentTitle: null | |
| }; | |
| if (typeof pdfjsLib !== 'undefined') { pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; } | |
| // --- 2. UI UTILITIES --- | |
| const UI = { | |
| toggleSidebar: () => { | |
| const sb = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('mobileOverlay'); | |
| sb.classList.toggle('collapsed'); | |
| overlay.classList.toggle('active', !sb.classList.contains('collapsed') && window.innerWidth <= 900); | |
| }, | |
| toggleAttachMenu: () => { | |
| document.getElementById('attachMenu').classList.toggle('active'); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| }, | |
| autoGrow: (el) => { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 120) + 'px'; }, | |
| scrollToBottom: () => { const c = document.getElementById('chatContainer'); c.scrollTop = c.scrollHeight; }, | |
| escape: (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'), | |
| showAuthMsg: (msg, isError=true) => { | |
| const el = document.getElementById('authMsg'); | |
| el.innerText = msg; el.style.color = isError ? 'var(--brand-danger)' : 'var(--brand-success)'; | |
| setTimeout(() => el.innerText='', 4000); | |
| }, | |
| updateUserInfo: (name, email) => { | |
| if(name) { State.name = name; localStorage.setItem('codeved_name', name); } | |
| const dispName = State.name || email.split('@')[0]; | |
| document.getElementById('uAv').innerText = dispName.charAt(0).toUpperCase(); | |
| document.getElementById('uName').innerText = dispName; | |
| document.getElementById('uSub').innerText = email; | |
| document.getElementById('uSub').style.color = "var(--text-secondary)"; | |
| document.getElementById('uSub').onclick = null; | |
| document.getElementById('uSub').style.cursor = 'default'; | |
| document.getElementById('btnLogout').style.display = 'flex'; | |
| document.getElementById('welcomeGreeting').innerText = `Ready, ${dispName}.`; | |
| }, | |
| // 🌟 Permanent Welcome Screen Fix | |
| updateWelcomeScreen: () => { | |
| const ws = document.getElementById('welcomeScreen'); | |
| if(State.history.length === 0) { | |
| ws.style.display = 'flex'; | |
| } else { | |
| ws.style.display = 'none'; | |
| } | |
| } | |
| }; | |
| document.addEventListener('click', (e) => { | |
| if(!e.target.closest('.tools-left')) { | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| } | |
| }); | |
| const inp = document.getElementById('mainInput'); | |
| inp.addEventListener('input', function() { UI.autoGrow(this); }); | |
| inp.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); Chat.handleSend(); } }); | |
| // --- 3. AUTH & MULTI-THREAD HISTORY --- | |
| const Auth = { | |
| init: () => { | |
| if (State.user) { | |
| UI.updateUserInfo(State.name, State.user); | |
| HistoryManager.syncAllChats(); | |
| } else { | |
| document.getElementById('uAv').innerText = "G"; | |
| document.getElementById('uName').innerText = "Guest Mode"; | |
| document.getElementById('uSub').innerText = `Queries: ${State.guestCount}/10`; | |
| document.getElementById('btnLogout').style.display = 'none'; | |
| document.getElementById('welcomeGreeting').innerText = `Engineered by Divy Patel`; | |
| } | |
| }, | |
| handleLogout: () => { if(confirm("Secure Logout?")) { localStorage.removeItem('codeved_user'); localStorage.removeItem('codeved_name'); location.reload(); } }, | |
| openModal: () => document.getElementById('authModal').style.display = 'flex', | |
| closeModal: () => document.getElementById('authModal').style.display = 'none', | |
| switchTab: (tab) => { | |
| document.getElementById('tabLogin').classList.remove('active'); document.getElementById('tabRegister').classList.remove('active'); | |
| document.getElementById('flowLogin').style.display = 'none'; document.getElementById('flowRegister').style.display = 'none'; | |
| if(tab === 'login') { document.getElementById('tabLogin').classList.add('active'); document.getElementById('flowLogin').style.display = 'block'; } | |
| else { document.getElementById('tabRegister').classList.add('active'); document.getElementById('flowRegister').style.display = 'block'; } | |
| }, | |
| switchPhase: (from, to) => { document.getElementById(from).classList.remove('active'); document.getElementById(to).classList.add('active'); }, | |
| process: async (action) => { | |
| let payload = { action: action }; let btnId = ''; | |
| if (action === 'register_send_otp') { | |
| payload.name = document.getElementById('regName').value.trim(); payload.email = document.getElementById('regEmail').value.trim(); | |
| payload.phone = "0000000000"; payload.organization = "CODE VED"; | |
| if (!payload.name || !payload.email) return UI.showAuthMsg("Details missing."); btnId = 'btnRegOtp'; | |
| } else if (action === 'login_send_otp') { | |
| payload.email = document.getElementById('logEmail').value.trim(); | |
| if (!payload.email) return UI.showAuthMsg("Email required."); btnId = 'btnLogOtp'; | |
| } else if (action === 'register_verify' || action === 'login_verify') { | |
| payload.email = document.getElementById(action === 'register_verify' ? 'regEmail' : 'logEmail').value.trim(); | |
| payload.otp = document.getElementById(action === 'register_verify' ? 'regOtp' : 'logOtp').value.trim(); | |
| if (!payload.otp) return UI.showAuthMsg("OTP required."); btnId = action === 'register_verify' ? 'btnRegVerify' : 'btnLogVerify'; | |
| } | |
| const btn = document.getElementById(btnId); const originalText = btn.innerText; | |
| btn.disabled = true; btn.innerText = "Wait..."; | |
| try { | |
| const res = await fetch(Config.GAS_URL, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify(payload) }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| UI.showAuthMsg(data.message, false); | |
| if (action === 'register_send_otp') Auth.switchPhase('regPhase1', 'regPhase2'); | |
| else if (action === 'login_send_otp') Auth.switchPhase('loginPhase1', 'loginPhase2'); | |
| else { | |
| localStorage.setItem('codeved_user', payload.email); | |
| if(data.user && data.user.name) localStorage.setItem('codeved_name', data.user.name); | |
| else if (payload.name) localStorage.setItem('codeved_name', payload.name); | |
| location.reload(); | |
| } | |
| } else { UI.showAuthMsg(data.message); } | |
| } catch (e) { UI.showAuthMsg("Network Error."); } | |
| btn.disabled = false; btn.innerText = originalText; | |
| } | |
| }; | |
| const HistoryManager = { | |
| syncAllChats: async () => { | |
| if(!State.user) return; | |
| try { | |
| const res = await fetch(Config.GAS_URL, { method: 'POST', body: JSON.stringify({action: "get_all_chats", email: State.user})}); | |
| const data = await res.json(); | |
| if(data.status === 'success') { | |
| if(data.user) UI.updateUserInfo(data.user.name, data.user.email); | |
| HistoryManager.renderSidebar(data.chats || []); | |
| } | |
| } catch(e) {} | |
| }, | |
| renderSidebar: (chats) => { | |
| const container = document.getElementById('chatHistory'); | |
| container.innerHTML = ''; | |
| if(chats.length > 0) { | |
| container.innerHTML = ` | |
| <div class="history-header"> | |
| <span class="history-title-text">Recent Workspaces</span> | |
| <button class="btn-clear-all" onclick="HistoryManager.clearAll()">Clear All</button> | |
| </div>`; | |
| } | |
| chats.forEach(chat => { | |
| const isActive = State.currentThreadId === chat.threadId ? 'active' : ''; | |
| container.innerHTML += ` | |
| <div class="history-item ${isActive}" onclick="HistoryManager.loadChat('${chat.threadId}')"> | |
| <span class="history-text">${UI.escape(chat.title)}</span> | |
| <button class="btn-delete-chat" onclick="event.stopPropagation(); HistoryManager.deleteChat('${chat.threadId}')" title="Delete Workspace"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg> | |
| </button> | |
| </div>`; | |
| }); | |
| }, | |
| loadChat: async (threadId) => { | |
| if(!State.user || State.isProcessing) return; | |
| State.currentThreadId = threadId; | |
| document.getElementById('chatMessages').innerHTML = ''; // Clear chat area | |
| try { | |
| const res = await fetch(Config.GAS_URL, { method: 'POST', body: JSON.stringify({action: "get_chat", email: State.user, threadId: threadId})}); | |
| const data = await res.json(); | |
| if(data.status === 'success') { | |
| State.history = JSON.parse(data.historyJSON || "[]"); | |
| UI.updateWelcomeScreen(); | |
| State.history.forEach(msg => { | |
| if(msg.role === 'user') { | |
| let dispText = msg.content; | |
| if(dispText.includes('[SYSTEM REAL-TIME WEATHER:')) dispText = dispText.split('\n\n[SYSTEM REAL-TIME WEATHER:')[0]; | |
| if(dispText.includes('---END DATA---')) dispText = dispText.split('User: ')[1] || '[Attached File]'; | |
| Chat.renderUser(dispText); | |
| } else { | |
| const botObj = Chat.renderBot(Date.now() + Math.random()); | |
| Chat.parseAndRender(msg.content, false, false, botObj.contentDiv); | |
| } | |
| }); | |
| HistoryManager.syncAllChats(); | |
| setTimeout(UI.scrollToBottom, 200); | |
| if(window.innerWidth <= 900) UI.toggleSidebar(); | |
| } | |
| } catch(e) {} | |
| }, | |
| startNew: () => { | |
| if(State.isProcessing) return; | |
| State.currentThreadId = null; | |
| State.currentTitle = null; | |
| State.history = []; | |
| document.getElementById('chatMessages').innerHTML = ''; | |
| UI.updateWelcomeScreen(); | |
| HistoryManager.syncAllChats(); | |
| if(window.innerWidth <= 900) UI.toggleSidebar(); | |
| }, | |
| saveCurrent: () => { | |
| if(!State.user || State.history.length === 0) return; | |
| if(!State.currentThreadId) { | |
| State.currentThreadId = "thr_" + Date.now(); | |
| const firstUser = State.history.find(m => m.role === 'user'); | |
| let rawText = firstUser ? firstUser.content : "New Workspace"; | |
| if(rawText.includes('[SYSTEM REAL-TIME WEATHER:')) rawText = rawText.split('\n\n[SYSTEM REAL-TIME WEATHER:')[0]; | |
| if(rawText.includes('---END DATA---')) rawText = rawText.split('User: ')[1] || "Document Analysis"; | |
| State.currentTitle = rawText.substring(0, 25) + (rawText.length > 25 ? '...' : ''); | |
| } | |
| fetch(Config.GAS_URL, { method: 'POST', body: JSON.stringify({ | |
| action: "save_chat", | |
| email: State.user, | |
| threadId: State.currentThreadId, | |
| title: State.currentTitle || "Active Workspace", | |
| historyJSON: JSON.stringify(State.history) | |
| })}).then(() => HistoryManager.syncAllChats()).catch(e=>{}); | |
| }, | |
| deleteChat: async (threadId) => { | |
| if(!confirm("Permanently delete this workspace?")) return; | |
| try { | |
| await fetch(Config.GAS_URL, { method: 'POST', body: JSON.stringify({action: "delete_chat", email: State.user, threadId: threadId})}); | |
| if(State.currentThreadId === threadId) HistoryManager.startNew(); | |
| else HistoryManager.syncAllChats(); | |
| } catch(e) {} | |
| }, | |
| clearAll: async () => { | |
| if(!confirm("WARNING: This will permanently delete ALL your workspaces. Continue?")) return; | |
| try { | |
| await fetch(Config.GAS_URL, { method: 'POST', body: JSON.stringify({action: "clear_all_chats", email: State.user})}); | |
| HistoryManager.startNew(); | |
| } catch(e) {} | |
| } | |
| }; | |
| // --- 4. 🌍 UNIFIED ENVIRONMENT MANAGER (GPS + WEATHER) --- | |
| const EnvironmentManager = { | |
| toggle: () => { | |
| const btn = document.getElementById('btnEnv'); | |
| const status = document.getElementById('locStatus'); | |
| if(State.location || State.weatherContext) { | |
| State.location = null; | |
| State.weatherContext = null; | |
| btn.classList.remove('active-env'); | |
| status.style.display = 'none'; | |
| } else { | |
| if(navigator.geolocation) { | |
| btn.style.color = 'var(--brand-warning)'; | |
| status.style.display = 'flex'; | |
| status.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg> <span class="flicker-text">Finding your exact location...</span>`; | |
| navigator.geolocation.getCurrentPosition(async pos => { | |
| State.location = { lat: pos.coords.latitude, lng: pos.coords.longitude }; | |
| status.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"></path></svg> <span class="flicker-text">Fetching real-time weather...</span>`; | |
| try { | |
| // Direct API call for Live Weather! | |
| const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${State.location.lat}&longitude=${State.location.lng}¤t_weather=true`); | |
| const data = await res.json(); | |
| const w = data.current_weather; | |
| State.weatherContext = `Temperature: ${w.temperature}°C, Wind Speed: ${w.windspeed} km/h`; | |
| btn.style.color = ''; | |
| btn.classList.add('active-env'); | |
| status.innerHTML = `<span style="color:var(--brand-success);">Location & Weather active!</span>`; | |
| setTimeout(() => status.style.display = 'none', 3000); | |
| } catch (e) { | |
| btn.style.color = ''; | |
| btn.classList.add('active-env'); | |
| status.innerHTML = `<span style="color:var(--brand-warning);">Location found, weather failed.</span>`; | |
| setTimeout(() => status.style.display = 'none', 3000); | |
| } | |
| }, err => { | |
| btn.style.color = ''; | |
| status.innerHTML = `<span style="color:var(--brand-danger);">Location access denied.</span>`; | |
| setTimeout(() => status.style.display = 'none', 3000); | |
| }); | |
| } else { alert("GPS not supported."); } | |
| } | |
| } | |
| }; | |
| const ThinkingManager = { | |
| toggleMenu: () => { | |
| document.getElementById('thinkMenu').classList.toggle('active'); | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| }, | |
| setEffort: (level) => { | |
| State.thinkingMode = true; | |
| State.thinkingEffort = level; | |
| const btn = document.getElementById('btnThink'); | |
| btn.classList.remove('active-think-low', 'active-think-medium', 'active-think-high'); | |
| btn.classList.add(`active-think-${level}`); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| }, | |
| disable: () => { | |
| State.thinkingMode = false; | |
| const btn = document.getElementById('btnThink'); | |
| btn.classList.remove('active-think-low', 'active-think-medium', 'active-think-high'); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| } | |
| }; | |
| // --- 5. FILE HANDLING & VOICE --- | |
| const FileSys = { | |
| process: async (input, type) => { | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| const file = input.files[0]; if (!file) return; | |
| document.getElementById('filePreviewBar').classList.add('active'); document.getElementById('fileName').innerText = file.name; | |
| const r = new FileReader(); | |
| if (type === 'image') { | |
| document.getElementById('imgPreview').style.display = 'flex'; document.getElementById('docPreview').style.display = 'none'; | |
| r.onload = (e) => { document.getElementById('imgPreview').src = e.target.result; State.attachment = { type: 'image', data: e.target.result.split(',')[1], name: file.name }; }; | |
| r.readAsDataURL(file); | |
| } else { | |
| document.getElementById('imgPreview').style.display = 'none'; document.getElementById('docPreview').style.display = 'flex'; | |
| r.onload = (e) => { State.attachment = { type: 'text', data: e.target.result, name: file.name }; }; r.readAsText(file); | |
| } | |
| input.value = ''; | |
| }, | |
| discard: () => { State.attachment = null; document.getElementById('filePreviewBar').classList.remove('active'); } | |
| }; | |
| const Speech = { | |
| rec: null, isRec: false, | |
| init: () => { | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SR) return alert("Speech Recognition not supported."); | |
| Speech.rec = new SR(); Speech.rec.continuous = false; Speech.rec.interimResults = true; Speech.rec.lang = 'en-US'; | |
| Speech.rec.onstart = () => { Speech.isRec = true; document.getElementById('btnStt').classList.add('recording'); }; | |
| Speech.rec.onresult = (e) => { | |
| let trans = ''; for (let i = e.resultIndex; i < e.results.length; ++i) { if (e.results[i].isFinal) trans += e.results[i][0].transcript; } | |
| if (trans) { const input = document.getElementById('mainInput'); input.value += (input.value ? ' ' : '') + trans; UI.autoGrow(input); } | |
| }; | |
| Speech.rec.onerror = () => Speech.stop(); Speech.rec.onend = () => Speech.stop(); | |
| }, | |
| toggle: () => { if (!Speech.rec) Speech.init(); if (Speech.isRec) Speech.stop(); else { try { Speech.rec.start(); } catch(e) {} } }, | |
| stop: () => { if(Speech.rec) Speech.rec.stop(); Speech.isRec = false; document.getElementById('btnStt').classList.remove('recording'); } | |
| }; | |
| // --- 6. WORKSPACE --- | |
| const Workspace = { | |
| open: (id) => { | |
| State.currentWs = id; const d = State.workspaces[id]; const b = document.getElementById('wsCodeBlock'); | |
| b.className = `hljs language-${d.lang || 'plaintext'}`; b.textContent = d.code; hljs.highlightElement(b); | |
| document.getElementById('workspacePanel').classList.add('active'); | |
| if(window.innerWidth <= 900) { document.getElementById('sidebar').classList.add('collapsed'); } | |
| Workspace.switchTab('code'); | |
| }, | |
| close: () => document.getElementById('workspacePanel').classList.remove('active'), | |
| switchTab: (tab) => { | |
| document.getElementById('tabCode').classList.remove('active'); document.getElementById('tabPreview').classList.remove('active'); | |
| if(tab === 'code') { | |
| document.getElementById('tabCode').classList.add('active'); | |
| document.getElementById('wsCodeView').style.display = 'block'; | |
| document.getElementById('wsPreviewView').style.display = 'none'; | |
| } else { | |
| document.getElementById('tabPreview').classList.add('active'); | |
| document.getElementById('wsCodeView').style.display = 'none'; | |
| document.getElementById('wsPreviewView').style.display = 'flex'; | |
| Workspace.run(); | |
| } | |
| }, | |
| copy: () => { | |
| if(State.currentWs !== null) { | |
| navigator.clipboard.writeText(State.workspaces[State.currentWs].code).then(() => alert('Code Copied!')); | |
| } | |
| }, | |
| run: async () => { | |
| if (State.currentWs === null) return; | |
| const d = State.workspaces[State.currentWs]; const lang = (d.lang || '').toLowerCase(); const view = document.getElementById('wsPreviewView'); | |
| if (['html', 'xml', 'javascript', 'js', 'css'].includes(lang)) { | |
| let html = d.code; | |
| if(lang === 'javascript' || lang === 'js') html = `<script>${d.code}<\/script>`; | |
| if(lang === 'css') html = `<style>${d.code}</style><div style="padding:20px; font-family:sans-serif;">CSS Applied. Add HTML to test.</div>`; | |
| const iframe = document.createElement('iframe'); iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.border = 'none'; | |
| view.innerHTML = ''; view.appendChild(iframe); iframe.contentWindow.document.open(); iframe.contentWindow.document.write(html); iframe.contentWindow.document.close(); | |
| return; | |
| } | |
| view.innerHTML = '<div class="ws-console" style="color:var(--text-tertiary);">Compiling code...</div>'; | |
| const pMap = {'python':'python', 'py':'python', 'c':'c', 'cpp':'c++', 'java':'java'}; const rLang = pMap[lang]; | |
| if(!rLang) { view.innerHTML = `<div class="ws-console" style="color:var(--brand-danger);">Live preview not supported for ${lang}.</div>`; return; } | |
| try { | |
| const res = await fetch('https://emkc.org/api/v2/piston/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language: rLang, version: "*", files: [{ content: d.code }] }) }); | |
| const data = await res.json(); | |
| let out = (data.compile?.output || "") + "\n" + (data.run?.output || "") + "\n" + (data.run?.stderr || ""); | |
| view.innerHTML = `<div class="ws-console">${UI.escape(out.trim() || "[Execution Finished]")}</div>`; | |
| } catch(e) { view.innerHTML = `<div class="ws-console" style="color:var(--brand-danger);">Execution Engine Error.</div>`; } | |
| } | |
| }; | |
| const MDRenderer = new marked.Renderer(); | |
| MDRenderer.code = function(code, lang) { | |
| const id = State.workspaces.length; State.workspaces.push({code: code, lang: lang}); | |
| const highlighted = hljs.highlightAuto(code).value; | |
| return ` | |
| <div style="margin: 16px 0; border: 1px solid var(--border-light); border-radius: 8px; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.02);"> | |
| <div style="background:var(--bg-sidebar); border-bottom: 1px solid var(--border-light); padding:8px 12px; display:flex; justify-content:space-between; align-items:center;"> | |
| <span style="font-size:12px; font-family:var(--font-code); color:var(--text-secondary); font-weight:600; text-transform:uppercase;">${lang || 'code'}</span> | |
| <button style="padding:4px 10px; font-size:11px; font-weight:500; background:#fff; border:1px solid var(--border-light); border-radius:4px; cursor:pointer;" onclick="Workspace.open(${id})">Open Canvas</button> | |
| </div> | |
| <pre style="margin:0;"><code class="hljs">${highlighted}</code></pre> | |
| </div>`; | |
| }; | |
| marked.use({ renderer: MDRenderer }); | |
| // --- 7. CHAT ENGINE (ROCK SOLID) --- | |
| const Chat = { | |
| renderUser: (txt, attachUI = '') => { | |
| const c = document.getElementById('chatMessages'); // Hardbound rendering | |
| const w = document.createElement('div'); w.className = 'message-wrapper'; | |
| w.innerHTML = `<div class="user-message">${attachUI}${txt ? UI.escape(txt) : ''}</div>`; | |
| c.appendChild(w); UI.scrollToBottom(); | |
| }, | |
| renderBot: (msgId) => { | |
| const c = document.getElementById('chatMessages'); | |
| const w = document.createElement('div'); w.className = 'message-wrapper'; | |
| const copyIcon = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`; | |
| const retryIcon = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path></svg>`; | |
| const stopIcon = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>`; | |
| w.innerHTML = ` | |
| <div class="bot-message"> | |
| <div class="bot-avatar"><img src="${Config.LOGO}"></div> | |
| <div style="flex:1; min-width:0;"> | |
| <div class="bot-content" id="bot-content-${msgId}"></div> | |
| <div class="msg-actions"> | |
| <button class="action-btn" onclick="Chat.copyMsg('${msgId}')">${copyIcon} Copy</button> | |
| <button class="action-btn" id="retry-btn-${msgId}" onclick="Chat.retryMsg()" style="display:none;">${retryIcon} Retry</button> | |
| <button class="action-btn stop-btn" id="stop-btn-${msgId}" onclick="Chat.stopMsg()">${stopIcon} Stop</button> | |
| </div> | |
| </div> | |
| </div>`; | |
| c.appendChild(w); UI.scrollToBottom(); | |
| return { | |
| contentDiv: document.getElementById(`bot-content-${msgId}`), | |
| stopBtn: document.getElementById(`stop-btn-${msgId}`), | |
| retryBtn: document.getElementById(`retry-btn-${msgId}`) | |
| }; | |
| }, | |
| getSearchHTML: (isActive) => { | |
| if(isActive) { | |
| return ` | |
| <div class="action-status active"> | |
| <svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> | |
| <span class="flicker-text">Searching on the web...</span> | |
| </div><br>`; | |
| } else { | |
| return ` | |
| <div class="action-status"> | |
| <svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg> | |
| <span>Search completed</span> | |
| </div><br>`; | |
| } | |
| }, | |
| parseAndRender: (fullText, isSearching, isProcessing, container) => { | |
| let finalHtml = ""; | |
| if (isSearching) finalHtml += Chat.getSearchHTML(isProcessing); | |
| let normalizedText = fullText | |
| .replace(/<\|channel\|>thought\s*<\|channel\|>/gi, "<think>\n") | |
| .replace(/<\|channel\|>answer\s*<\|channel\|>/gi, "\n</think>\n") | |
| .replace(/<\|im_start\|>thought/gi, "<think>\n") | |
| .replace(/<\|im_end\|>/gi, "\n</think>\n"); | |
| const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/i; | |
| const thinkMatch = normalizedText.match(thinkRegex); | |
| if (thinkMatch) { | |
| const thinkContent = thinkMatch[1].trim(); | |
| const isOpen = isProcessing ? 'open' : ''; | |
| const arrowIcon = `<svg class="arrow" style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>`; | |
| const brainIcon = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a5 5 0 0 0-5 5v2a5 5 0 0 0 10 0V7a5 5 0 0 0-5-5z"></path></svg>`; | |
| finalHtml += ` | |
| <details class="qwen-think-box" ${isOpen}> | |
| <summary>${arrowIcon} ${brainIcon} Thinking Process</summary> | |
| <div class="qwen-think-content">${marked.parse(thinkContent || '...')}</div> | |
| </details>`; | |
| } | |
| let mainText = normalizedText.replace(thinkRegex, '').trim(); | |
| if(mainText) finalHtml += marked.parse(mainText); | |
| if(isProcessing && !thinkMatch && !mainText) { | |
| finalHtml += `<div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div></div>`; | |
| } | |
| container.innerHTML = finalHtml; | |
| }, | |
| copyMsg: (id) => { | |
| const text = document.getElementById(`bot-content-${id}`).innerText; | |
| navigator.clipboard.writeText(text).then(() => alert('Copied to clipboard.')); | |
| }, | |
| stopMsg: () => { | |
| if(State.abortController) { State.abortController.abort(); } | |
| }, | |
| retryMsg: () => { | |
| if(State.isProcessing || !State.lastUserMessage) return; | |
| if(State.history.length > 0 && State.history[State.history.length-1].role === 'assistant') { | |
| State.history.pop(); | |
| } | |
| document.getElementById('mainInput').value = State.lastUserMessage; | |
| Chat.handleSend(true); | |
| }, | |
| handleSend: async (isRetry = false) => { | |
| if(State.isProcessing) return; | |
| const input = document.getElementById('mainInput'); | |
| const text = input.value.trim(); | |
| if(!text && !State.attachment) return; | |
| if(!State.user) { | |
| if(State.guestCount >= 10) { Auth.openModal(); UI.showAuthMsg("Guest limit reached. Login required.", true); return; } | |
| State.guestCount++; localStorage.setItem('codeved_guest', State.guestCount); | |
| document.getElementById('uSub').innerText = `Queries: ${State.guestCount}/10`; | |
| } | |
| State.isProcessing = true; document.getElementById('btnSend').disabled = true; | |
| State.lastUserMessage = text; | |
| input.value = ''; input.style.height = 'auto'; | |
| UI.updateWelcomeScreen(); | |
| const searchTriggers = ["search", "latest", "news", "near", "restaurant", "shop", "distance", "time", "आसपास", "दूरी", "दुकान"]; | |
| let isSearching = searchTriggers.some(kw => text.toLowerCase().includes(kw)); | |
| let payloadStr = text; | |
| let attachUI = ''; | |
| let mediaArray = []; | |
| if(State.attachment) { | |
| if(State.attachment.type === 'text') { | |
| payloadStr = `[File attached: ${State.attachment.name}]\n\n---DATA---\n${State.attachment.data}\n---END DATA---\n\nUser: ${text}`; | |
| attachUI = `<div class="chat-attachment"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path></svg> ${State.attachment.name}</div><br>`; | |
| } else { | |
| mediaArray.push(State.attachment); | |
| attachUI = `<img src="data:image/jpeg;base64,${State.attachment.data}" class="chat-img-preview"><br>`; | |
| } | |
| } | |
| // 🌤️ Inject Weather Context Safely | |
| let systemInjectedPayload = payloadStr; | |
| if(State.weatherContext) { | |
| systemInjectedPayload += `\n\n[SYSTEM REAL-TIME WEATHER: ${State.weatherContext}]`; | |
| } | |
| if(!isRetry) { | |
| // Only show the user's text on screen, NOT the system tags | |
| Chat.renderUser(text, attachUI); | |
| State.history.push({ role: 'user', content: systemInjectedPayload }); | |
| } | |
| FileSys.discard(); | |
| const msgId = Date.now().toString(); | |
| const botObj = Chat.renderBot(msgId); | |
| Chat.parseAndRender("", isSearching, true, botObj.contentDiv); | |
| State.abortController = new AbortController(); | |
| try { | |
| const res = await fetch(Config.API_ENDPOINT, { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| message: systemInjectedPayload, | |
| attachments: mediaArray, | |
| is_search: isSearching, | |
| location: State.location, | |
| thinking_mode: State.thinkingMode, | |
| thinking_effort: State.thinkingEffort, | |
| history: State.history | |
| }), | |
| signal: State.abortController.signal | |
| }); | |
| if(!res.ok) throw new Error("API Offline"); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullText = ""; | |
| while(true) { | |
| const {done, value} = await reader.read(); | |
| if(done) break; | |
| const chunk = decoder.decode(value, {stream: true}); | |
| const lines = chunk.split('\n'); | |
| for(let line of lines) { | |
| if(line.startsWith('data: ')) { | |
| const dataStr = line.substring(6).trim(); | |
| if(dataStr === '[DONE]') continue; | |
| try { | |
| const json = JSON.parse(dataStr); | |
| if(json.choices && json.choices[0].delta.content) fullText += json.choices[0].delta.content; | |
| Chat.parseAndRender(fullText, isSearching, true, botObj.contentDiv); | |
| UI.scrollToBottom(); | |
| } catch(e){ | |
| // Safe parsing: Ignore incomplete chunks during stream | |
| } | |
| } | |
| } | |
| } | |
| Chat.parseAndRender(fullText, isSearching, false, botObj.contentDiv); | |
| State.history.push({ role: 'assistant', content: fullText }); | |
| HistoryManager.saveCurrent(); | |
| } catch(e) { | |
| if (e.name === 'AbortError') { | |
| botObj.contentDiv.innerHTML += `<br><br><span style="color:var(--text-tertiary); font-size:13px; font-weight:500;">[Generation Stopped]</span>`; | |
| } else { | |
| botObj.contentDiv.innerHTML = `<span style="color:var(--brand-danger);">Connection Offline.</span>`; | |
| } | |
| } | |
| finally { | |
| State.isProcessing = false; | |
| document.getElementById('btnSend').disabled = false; | |
| if(botObj.stopBtn) botObj.stopBtn.style.display = 'none'; | |
| if(botObj.retryBtn) botObj.retryBtn.style.display = 'flex'; | |
| UI.scrollToBottom(); | |
| } | |
| } | |
| }; | |
| window.onload = () => { | |
| Auth.init(); | |
| UI.updateWelcomeScreen(); | |
| if(window.innerWidth > 900) { document.getElementById('sidebar').classList.remove('collapsed'); } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |