CodeVed / index.html
Vedika66's picture
Update index.html
33737bb verified
Raw
History Blame Contribute Delete
75.3 kB
<!DOCTYPE html>
<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'),
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}&current_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>