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> | |
| <!-- Google 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:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <!-- Highlight.js CSS (डार्क थीम) --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.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> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | |
| <!-- MathJax कॉन्फ़िगरेशन (गणित, भौतिकी, रसायन विज्ञान के लिए) --> | |
| <script> | |
| window.MathJax = { | |
| tex: { | |
| inlineMath: [ | |
| ['$', '$'], | |
| ['\\(', '\\)'] | |
| ], | |
| displayMath: [ | |
| ['$$', '$$'], | |
| ['\\[', '\\]'] | |
| ], | |
| processEscapes: true, | |
| packages: { | |
| '[+]': ['mhchem', 'physics', 'cancel', 'color', 'amsmath', 'amssymb', 'boldsymbol'] | |
| } | |
| }, | |
| loader: { | |
| load: ['[tex]/mhchem', '[tex]/physics', '[tex]/cancel', '[tex]/color', '[tex]/amsmath', '[tex]/amssymb', '[tex]/boldsymbol'] | |
| }, | |
| options: { | |
| ignoreHtmlClass: 'tex2jax_ignore', | |
| processHtmlClass: 'tex2jax_process' | |
| }, | |
| startup: { | |
| typeset: false | |
| } | |
| }; | |
| </script> | |
| <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | |
| <!-- सीएसएस स्टाइल्स --> | |
| <style> | |
| :root { | |
| /* कलर्स और थीम्स */ | |
| --bg-main: #ffffff; | |
| --bg-sidebar: rgba(240, 244, 249, 0.7); | |
| --bg-user-msg: rgba(240, 244, 249, 0.8); | |
| --text-primary: #1f1f1f; | |
| --text-secondary: #444746; | |
| --text-tertiary: #727775; | |
| --border-light: rgba(116, 119, 117, 0.15); | |
| --border-focus: #a8c7fa; | |
| --brand-color: #0f172a; | |
| --brand-accent: #0b57d0; | |
| --brand-success: #146c2e; | |
| --brand-danger: #b3261e; | |
| --brand-warning: #e37400; | |
| /* फोंट्स */ | |
| --font-ui: 'Inter', sans-serif; | |
| --font-code: 'JetBrains Mono', monospace; | |
| /* रेडियस और ट्रांज़िशन */ | |
| --radius-sm: 12px; | |
| --radius-md: 18px; | |
| --radius-lg: 32px; | |
| --transition-default: 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| /* ग्लासमोर्फिज़्म प्रभाव */ | |
| --glass-bg: rgba(255, 255, 255, 0.6); | |
| --glass-border: rgba(255, 255, 255, 0.3); | |
| --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| } | |
| /* ग्लोबल रीसेट */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| *:focus { | |
| outline: none ; | |
| box-shadow: none ; | |
| } | |
| *:focus-visible { | |
| outline: none ; | |
| box-shadow: none ; | |
| } | |
| /* बॉडी और लेआउट */ | |
| html, body { | |
| height: 100%; | |
| height: 100dvh; | |
| width: 100vw; | |
| font-family: var(--font-ui); | |
| color: var(--text-primary); | |
| display: flex; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| body { | |
| background: linear-gradient(180deg, #ffffff 0%, #ffffff 40%, #e3ecfa 100%); | |
| position: relative; | |
| } | |
| /* बैकग्राउंड एनिमेशन */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: | |
| radial-gradient(circle at 20% 30%, rgba(168, 199, 250, 0.15) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 70%, rgba(11, 87, 208, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 50% 50%, rgba(227, 236, 250, 0.2) 0%, transparent 70%); | |
| animation: liquidMove 20s ease-in-out infinite; | |
| pointer-events: none; | |
| z-index: 0; | |
| will-change: transform; | |
| backface-visibility: hidden; | |
| transform: translate3d(0, 0, 0); | |
| } | |
| @keyframes liquidMove { | |
| 0%, 100% { | |
| transform: translate(0, 0) rotate(0deg); | |
| } | |
| 33% { | |
| transform: translate(30px, -30px) rotate(120deg); | |
| } | |
| 66% { | |
| transform: translate(-20px, 20px) rotate(240deg); | |
| } | |
| } | |
| button, a, [role="button"] { | |
| -webkit-tap-highlight-color: transparent; | |
| outline: none; | |
| } | |
| 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: rgba(114, 119, 117, 0.3); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(114, 119, 117, 0.6); | |
| } | |
| /* एनिमेशन कीफ्रेम्स */ | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(6px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(12px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes pulseOpacity { | |
| 0% { | |
| opacity: 0.4; | |
| } | |
| 100% { | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes spin { | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| @keyframes typingBounce { | |
| 0%, 60%, 100% { | |
| transform: translateY(0); | |
| opacity: 0.4; | |
| } | |
| 30% { | |
| transform: translateY(-4px); | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes recordPulse { | |
| 0% { | |
| transform: scale(1); | |
| box-shadow: 0 0 0 0 rgba(179, 38, 30, 0.4); | |
| } | |
| 50% { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 0 10px rgba(179, 38, 30, 0); | |
| } | |
| 100% { | |
| transform: scale(1); | |
| box-shadow: 0 0 0 0 rgba(179, 38, 30, 0); | |
| } | |
| } | |
| @keyframes dropUpFade { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px) scale(0.95); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| } | |
| } | |
| @keyframes modalFadeIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.96); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| @keyframes blinkCursor { | |
| 0%, 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0; | |
| } | |
| } | |
| .blinking-cursor { | |
| display: inline-block; | |
| width: 8px; | |
| height: 20px; | |
| background: var(--brand-accent); | |
| margin-left: 2px; | |
| vertical-align: text-bottom; | |
| animation: blinkCursor 0.8s infinite; | |
| border-radius: 2px; | |
| } | |
| /* मेन्यू बटन */ | |
| .btn-menu { | |
| position: absolute; | |
| top: 16px; | |
| left: 16px; | |
| z-index: 50; | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid var(--glass-border); | |
| color: var(--text-secondary); | |
| transition: var(--transition-default); | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .btn-menu:hover { | |
| background: rgba(255, 255, 255, 0.8); | |
| color: var(--text-primary); | |
| } | |
| /* साइडबार स्टाइल्स */ | |
| .sidebar { | |
| width: 280px; | |
| background: var(--bg-sidebar); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border-right: 1px solid var(--glass-border); | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| z-index: 100; | |
| height: 100%; | |
| flex-shrink: 0; | |
| box-shadow: 2px 0 12px rgba(0, 0, 0, 0.03); | |
| } | |
| .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: 16px; | |
| letter-spacing: -0.3px; | |
| color: var(--text-primary); | |
| } | |
| .btn-new-chat { | |
| margin: 10px 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| padding: 12px 14px; | |
| border-radius: var(--radius-sm); | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| font-size: 14px; | |
| font-weight: 500; | |
| transition: var(--transition-default); | |
| color: var(--text-primary); | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .btn-new-chat:hover { | |
| background: rgba(255, 255, 255, 0.9); | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); | |
| color: var(--brand-accent); | |
| border-color: var(--border-focus); | |
| } | |
| .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: var(--transition-default); | |
| } | |
| .btn-clear-all:hover { | |
| color: var(--brand-danger); | |
| } | |
| .history-item { | |
| padding: 10px 14px; | |
| border-radius: var(--radius-sm); | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| transition: var(--transition-default); | |
| border: 1px solid transparent; | |
| } | |
| .history-item:hover { | |
| background: rgba(255, 255, 255, 0.6); | |
| color: var(--text-primary); | |
| } | |
| .history-item.active { | |
| background: rgba(227, 236, 250, 0.8); | |
| color: #001d35; | |
| font-weight: 500; | |
| border-color: rgba(219, 234, 254, 0.5); | |
| } | |
| .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: 6px; | |
| } | |
| .history-item:hover .btn-delete-chat { | |
| display: flex; | |
| } | |
| .btn-delete-chat:hover { | |
| color: var(--brand-danger); | |
| background: #fce8e8; | |
| } | |
| .sidebar-footer { | |
| padding: 16px; | |
| border-top: 1px solid var(--glass-border); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| flex-wrap: wrap; | |
| } | |
| .user-avatar { | |
| width: 38px; | |
| height: 38px; | |
| border-radius: 50%; | |
| background: var(--brand-accent); | |
| color: #fff; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 15px; | |
| font-weight: 600; | |
| flex-shrink: 0; | |
| } | |
| .user-info-box { | |
| flex: 1; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| .user-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| color: var(--text-primary); | |
| line-height: 1.2; | |
| } | |
| .user-sub { | |
| font-size: 11.5px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-top: 2px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| line-height: 1.2; | |
| cursor: pointer; | |
| } | |
| .btn-login-register { | |
| background: var(--brand-accent); | |
| color: #fff; | |
| border-radius: 20px; | |
| padding: 6px 14px; | |
| font-size: 11.5px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition-default); | |
| letter-spacing: -0.2px; | |
| white-space: nowrap; | |
| margin-left: auto; | |
| box-shadow: 0 2px 8px rgba(11, 87, 208, 0.15); | |
| } | |
| .btn-login-register:hover { | |
| background: #0842a0; | |
| transform: scale(1.02); | |
| } | |
| .btn-power { | |
| width: 34px; | |
| height: 34px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-secondary); | |
| transition: var(--transition-default); | |
| } | |
| .btn-power:hover { | |
| background: #fce8e8; | |
| color: var(--brand-danger); | |
| } | |
| /* मेन एरिया स्टाइल्स */ | |
| .main-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| transition: 0.3s; | |
| height: 100%; | |
| overflow: hidden; | |
| z-index: 1; | |
| } | |
| .chat-container { | |
| flex: 1; | |
| overflow-x: hidden; | |
| overflow-y: auto; | |
| padding: 60px 20px 180px; | |
| 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: fadeInUp 0.6s ease; | |
| width: 100%; | |
| justify-content: center; | |
| flex: 1; | |
| } | |
| .welcome-center img { | |
| width: 64px; | |
| height: 64px; | |
| border-radius: 18px; | |
| margin-bottom: 24px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| object-fit: cover; | |
| } | |
| .welcome-center h1 { | |
| font-size: 32px; | |
| font-weight: 400; | |
| color: #1f1f1f; | |
| margin-bottom: 8px; | |
| line-height: 1.3; | |
| } | |
| .welcome-center p { | |
| font-size: 16px; | |
| color: var(--text-secondary); | |
| display: none; | |
| } | |
| #chatMessages { | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .message-wrapper { | |
| width: 100%; | |
| max-width: 850px; | |
| margin-bottom: 32px; | |
| display: flex; | |
| flex-direction: column; | |
| animation: fadeInUp 0.3s ease; | |
| } | |
| .user-message { | |
| align-self: flex-end; | |
| max-width: 85%; | |
| background: var(--bg-user-msg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| padding: 14px 20px; | |
| border-radius: 24px 24px 6px 24px; | |
| font-size: 15.5px; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .bot-message { | |
| align-self: flex-start; | |
| max-width: 100%; | |
| display: flex; | |
| gap: 16px; | |
| width: 100%; | |
| box-sizing: border-box; | |
| background: transparent; | |
| padding: 0; | |
| border: none; | |
| box-shadow: none; | |
| margin-top: 4px; | |
| } | |
| .bot-avatar { | |
| width: 34px; | |
| height: 34px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| overflow: hidden; | |
| border: 1px solid var(--border-light); | |
| background: #fff; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); | |
| margin-top: 2px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .bot-avatar img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .bot-content-wrapper { | |
| flex: 1 1 0%; | |
| min-width: 0; | |
| max-width: calc(100% - 50px); | |
| width: 100%; | |
| overflow: hidden; | |
| } | |
| .bot-content { | |
| font-size: 15.5px; | |
| line-height: 1.7; | |
| color: var(--text-primary); | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| word-break: normal; | |
| white-space: normal; | |
| width: 100%; | |
| max-width: 100%; | |
| min-height: 20px; | |
| } | |
| .bot-content * { | |
| max-width: 100%; | |
| } | |
| .bot-content p { | |
| margin-bottom: 16px; | |
| white-space: normal; | |
| } | |
| .bot-content p:last-child { | |
| margin-bottom: 0; | |
| } | |
| .bot-content strong { | |
| font-weight: 600; | |
| color: #000; | |
| } | |
| .bot-content em { | |
| font-style: italic; | |
| } | |
| /* मार्कडाउन लिस्ट्स */ | |
| .bot-content ul, .bot-content ol { | |
| padding-left: 24px; | |
| margin-bottom: 16px; | |
| } | |
| .bot-content li { | |
| margin-bottom: 8px; | |
| } | |
| .bot-content li:last-child { | |
| margin-bottom: 0; | |
| } | |
| .bot-content code { | |
| font-family: var(--font-code); | |
| background: rgba(31, 31, 31, 0.05); | |
| padding: 3px 6px; | |
| border-radius: 6px; | |
| font-size: 0.9em; | |
| color: #0284c7; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| .bot-content pre { | |
| background: #0d1117; | |
| padding: 40px 16px 16px; | |
| border-radius: var(--radius-md); | |
| overflow-x: auto; | |
| margin: 16px 0; | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); | |
| position: relative; | |
| white-space: pre; | |
| word-break: normal; | |
| } | |
| .bot-content pre::before { | |
| content: ''; | |
| position: absolute; | |
| top: 14px; | |
| left: 16px; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: #ff5f56; | |
| box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f; | |
| } | |
| .bot-content pre code { | |
| background: none; | |
| padding: 0; | |
| color: #e2e8f0; | |
| font-size: 13.5px; | |
| text-shadow: none; | |
| white-space: pre; | |
| } | |
| .bot-content .mermaid { | |
| background: #f5f7f9; | |
| padding: 16px; | |
| border-radius: var(--radius-md); | |
| margin: 16px 0; | |
| text-align: center; | |
| overflow-x: auto; | |
| } | |
| .bot-content table { | |
| display: block; | |
| width: 100%; | |
| overflow-x: auto; | |
| border-collapse: collapse; | |
| margin: 16px 0; | |
| white-space: normal; | |
| -webkit-overflow-scrolling: touch; | |
| border: 1px solid var(--border-light); | |
| border-radius: 8px; | |
| } | |
| .bot-content th, | |
| .bot-content td { | |
| border: 1px solid var(--border-light); | |
| padding: 10px 14px; | |
| text-align: left; | |
| background: rgba(255, 255, 255, 0.4); | |
| } | |
| .bot-content th { | |
| background: rgba(240, 244, 249, 0.7); | |
| font-weight: 600; | |
| } | |
| .mjx-container { | |
| max-width: 100%; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| } | |
| /* अटैचमेंट कंटेनर */ | |
| .chat-attachment-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| .chat-file-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| border: 1px solid var(--glass-border); | |
| padding: 8px 14px; | |
| border-radius: var(--radius-lg); | |
| font-size: 13px; | |
| font-weight: 500; | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03); | |
| } | |
| .chat-img-preview { | |
| max-width: 120px; | |
| max-height: 120px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border-light); | |
| object-fit: cover; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); | |
| } | |
| /* थिंकिंग बॉक्स स्टाइल */ | |
| .qwen-think-box { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 16px; | |
| margin-bottom: 16px; | |
| overflow: hidden; | |
| transition: var(--transition-default); | |
| } | |
| .qwen-think-box summary { | |
| padding: 12px 16px; | |
| font-size: 13.5px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| user-select: none; | |
| list-style: none; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(248, 250, 252, 0.4); | |
| } | |
| .qwen-think-box summary::-webkit-details-marker { | |
| display: none; | |
| } | |
| .qwen-think-box summary svg.arrow { | |
| width: 14px; | |
| height: 14px; | |
| transition: transform var(--transition-default); | |
| } | |
| .qwen-think-box[open] summary svg.arrow { | |
| transform: rotate(90deg); | |
| } | |
| .qwen-think-content { | |
| padding: 16px; | |
| border-top: 1px solid var(--border-light); | |
| font-size: 13.5px; | |
| color: var(--text-secondary); | |
| font-style: italic; | |
| line-height: 1.6; | |
| opacity: 0.8; | |
| white-space: pre-wrap; | |
| } | |
| .action-status { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| border-radius: 16px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 16px; | |
| border: 1px solid var(--glass-border); | |
| cursor: default; | |
| user-select: none; | |
| } | |
| .action-status.active { | |
| background: rgba(227, 236, 250, 0.8); | |
| border-color: rgba(168, 199, 250, 0.5); | |
| color: var(--brand-accent); | |
| } | |
| .flicker-text { | |
| animation: pulseOpacity 1.2s infinite alternate; | |
| } | |
| .spin-icon { | |
| animation: spin 1s linear infinite; | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| gap: 4px; | |
| padding: 8px 0; | |
| align-items: center; | |
| } | |
| .typing-dot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: var(--text-tertiary); | |
| animation: typingBounce 1.2s infinite; | |
| } | |
| .typing-dot:nth-child(2) { | |
| animation-delay: 0.15s; | |
| } | |
| .typing-dot:nth-child(3) { | |
| animation-delay: 0.3s; | |
| } | |
| .msg-actions { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 14px; | |
| opacity: 0; | |
| transition: opacity var(--transition-default); | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .message-wrapper:hover .msg-actions { | |
| opacity: 1; | |
| } | |
| @media (hover: none) { | |
| .msg-actions { | |
| opacity: 1; | |
| } | |
| } | |
| .action-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| padding: 8px 14px; | |
| border-radius: 12px; | |
| transition: var(--transition-default); | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(4px); | |
| -webkit-backdrop-filter: blur(4px); | |
| border: 1px solid var(--glass-border); | |
| } | |
| .action-btn:hover { | |
| background: rgba(255, 255, 255, 0.9); | |
| color: var(--text-primary); | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); | |
| } | |
| .action-btn.listen-btn { | |
| background: rgba(227, 236, 250, 0.8); | |
| color: #0b57d0; | |
| border-color: transparent; | |
| } | |
| .action-btn.listen-btn:hover { | |
| background: rgba(211, 227, 253, 0.9); | |
| } | |
| /* बॉटम पर स्क्रॉल करने का बटन */ | |
| .btn-scroll-bottom { | |
| position: absolute; | |
| bottom: 110px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(20px); | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid var(--glass-border); | |
| box-shadow: var(--glass-shadow); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-secondary); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.3s ease; | |
| z-index: 45; | |
| } | |
| .btn-scroll-bottom.visible { | |
| opacity: 1; | |
| pointer-events: auto; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .btn-scroll-bottom:hover { | |
| background: rgba(255, 255, 255, 0.9); | |
| color: var(--brand-accent); | |
| } | |
| /* इनपुट एरिया */ | |
| .input-dock { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 10px 20px 24px; | |
| padding-bottom: max(24px, env(safe-area-inset-bottom)); | |
| background: linear-gradient(to top, rgba(17, 25, 40, 0.18) 0%, transparent 100%); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| z-index: 50; | |
| } | |
| .loc-toast { | |
| display: none; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 12.5px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 12px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| padding: 6px 14px; | |
| border-radius: 16px; | |
| border: 1px solid var(--glass-border); | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .input-container { | |
| width: 100%; | |
| max-width: 850px; | |
| background: rgba(255, 255, 255, 0.82); | |
| backdrop-filter: blur(24px); | |
| -webkit-backdrop-filter: blur(24px); | |
| border: 1px solid rgba(255, 255, 255, 0.4); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(168, 199, 250, 0.15); | |
| border-radius: 50px; | |
| padding: 8px 12px 8px 16px; | |
| display: flex; | |
| flex-direction: column; | |
| transition: var(--transition-default); | |
| } | |
| .input-container:focus-within { | |
| box-shadow: 0 8px 24px rgba(11, 87, 208, 0.15), 0 0 0 2px rgba(168, 199, 250, 0.5); | |
| border-color: rgba(168, 199, 250, 0.6); | |
| background: rgba(255, 255, 255, 0.92); | |
| } | |
| .file-preview-bar { | |
| display: none; | |
| padding: 4px 0 12px; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 4px; | |
| } | |
| .file-preview-bar.active { | |
| display: flex; | |
| } | |
| .file-preview-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| border: 1px solid var(--border-light); | |
| padding: 6px 12px; | |
| border-radius: 16px; | |
| font-size: 13.5px; | |
| font-weight: 500; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03); | |
| } | |
| .file-preview-icon { | |
| width: 18px; | |
| height: 18px; | |
| object-fit: cover; | |
| border-radius: 4px; | |
| } | |
| .file-preview-name { | |
| max-width: 120px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| color: var(--text-primary); | |
| } | |
| .btn-remove-file { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #fce8e8; | |
| color: var(--brand-danger); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition-default); | |
| margin-left: 4px; | |
| } | |
| .btn-remove-file:hover { | |
| background: #fca5a5; | |
| color: #fff; | |
| } | |
| .input-row { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 10px; | |
| width: 100%; | |
| align-items: center; | |
| } | |
| .tools-left { | |
| display: flex; | |
| gap: 4px; | |
| align-items: center; | |
| position: relative; | |
| } | |
| .tool-btn { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-secondary); | |
| transition: all 0.2s ease; | |
| position: relative; | |
| flex-shrink: 0; | |
| } | |
| .tool-btn:hover { | |
| background: rgba(255, 255, 255, 0.7); | |
| color: var(--text-primary); | |
| transform: scale(1.05); | |
| } | |
| .tool-btn.active-env { | |
| color: #fff; | |
| background: rgba(11, 87, 208, 0.7); | |
| } | |
| .tool-btn.active-think-low { | |
| color: #fff; | |
| background: rgba(179, 38, 30, 0.7); | |
| } | |
| .tool-btn.active-think-medium { | |
| color: #fff; | |
| background: rgba(227, 116, 0, 0.7); | |
| } | |
| .tool-btn.active-think-high { | |
| color: #fff; | |
| background: rgba(11, 87, 208, 0.7); | |
| } | |
| .tool-btn.active-search { | |
| color: #fff; | |
| background: rgba(20, 108, 46, 0.7); | |
| } | |
| .btn-mic.recording { | |
| color: var(--brand-danger); | |
| background: rgba(252, 232, 232, 0.9); | |
| animation: recordPulse 1.5s infinite; | |
| } | |
| .chat-input { | |
| flex: 1; | |
| padding: 12px 4px; | |
| font-size: 15.5px; | |
| resize: none; | |
| max-height: 180px; | |
| min-height: 24px; | |
| color: var(--text-primary); | |
| line-height: 1.5; | |
| background: transparent; | |
| caret-color: var(--brand-accent); | |
| } | |
| .chat-input::placeholder { | |
| color: var(--text-tertiary); | |
| font-weight: 400; | |
| } | |
| .btn-send { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| background: transparent; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| flex-shrink: 0; | |
| } | |
| .btn-send:hover:not(:disabled) { | |
| background: rgba(255, 255, 255, 0.8); | |
| transform: scale(1.08); | |
| } | |
| .btn-send:disabled { | |
| color: var(--text-tertiary); | |
| cursor: not-allowed; | |
| background: transparent; | |
| } | |
| .btn-stop { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| background: rgba(252, 232, 232, 0.9); | |
| color: var(--brand-danger); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| flex-shrink: 0; | |
| } | |
| .btn-stop.active { | |
| display: flex; | |
| } | |
| .btn-stop:hover { | |
| background: #fca5a5; | |
| color: #fff; | |
| } | |
| .hidden-input { | |
| display: none; | |
| } | |
| /* ड्रॉपडाउन मेनू */ | |
| .dropdown-menu { | |
| position: absolute; | |
| bottom: calc(100% + 14px); | |
| left: 0; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 20px; | |
| padding: 8px; | |
| box-shadow: var(--glass-shadow); | |
| display: none; | |
| flex-direction: column; | |
| min-width: 200px; | |
| z-index: 100; | |
| transform-origin: bottom left; | |
| } | |
| .dropdown-menu.active { | |
| display: flex; | |
| animation: dropUpFade 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .dropdown-header { | |
| padding: 8px 14px 12px; | |
| font-size: 11.5px; | |
| font-weight: 600; | |
| color: var(--text-tertiary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| border-bottom: 1px solid var(--border-light); | |
| margin-bottom: 4px; | |
| } | |
| .dropdown-item { | |
| padding: 12px 14px; | |
| border-radius: 12px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| cursor: pointer; | |
| transition: var(--transition-default); | |
| color: var(--text-secondary); | |
| } | |
| .dropdown-item:hover { | |
| background: rgba(255, 255, 255, 0.6); | |
| color: var(--text-primary); | |
| } | |
| .dropdown-item.selected { | |
| background: rgba(227, 236, 250, 0.8); | |
| color: var(--brand-accent); | |
| font-weight: 600; | |
| } | |
| .dropdown-item svg { | |
| color: inherit; | |
| } | |
| /* ऑथेंटिकेशन (लॉगिन/रजिस्टर) मोडल */ | |
| .auth-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(15, 23, 42, 0.5); | |
| backdrop-filter: blur(8px); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| } | |
| .auth-modal { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(24px); | |
| -webkit-backdrop-filter: blur(24px); | |
| width: 90%; | |
| max-width: 380px; | |
| border-radius: var(--radius-lg); | |
| padding: 32px; | |
| box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15); | |
| position: relative; | |
| border: 1px solid var(--glass-border); | |
| animation: modalFadeIn 0.2s ease; | |
| } | |
| .auth-close { | |
| position: absolute; | |
| top: 16px; | |
| right: 16px; | |
| color: var(--text-tertiary); | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| transition: var(--transition-default); | |
| } | |
| .auth-close:hover { | |
| background: var(--bg-sidebar); | |
| color: var(--text-primary); | |
| } | |
| .auth-title { | |
| font-size: 22px; | |
| font-weight: 600; | |
| margin-bottom: 24px; | |
| text-align: center; | |
| color: var(--text-primary); | |
| letter-spacing: -0.5px; | |
| } | |
| .auth-tabs { | |
| display: flex; | |
| background: rgba(240, 244, 249, 0.6); | |
| border-radius: var(--radius-sm); | |
| padding: 4px; | |
| margin-bottom: 20px; | |
| } | |
| .auth-tab { | |
| flex: 1; | |
| text-align: center; | |
| padding: 10px; | |
| font-size: 13.5px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| transition: var(--transition-default); | |
| } | |
| .auth-tab.active { | |
| background: rgba(255, 255, 255, 0.9); | |
| color: var(--text-primary); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| } | |
| .auth-input { | |
| width: 100%; | |
| padding: 14px 16px; | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| font-size: 14.5px; | |
| margin-bottom: 12px; | |
| transition: var(--transition-default); | |
| background: rgba(248, 250, 252, 0.8); | |
| caret-color: var(--brand-accent); | |
| } | |
| .auth-input:focus { | |
| border-color: var(--brand-accent); | |
| background: rgba(255, 255, 255, 0.95); | |
| box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); | |
| } | |
| .auth-btn { | |
| width: 100%; | |
| padding: 14px; | |
| background: var(--brand-color); | |
| color: #fff; | |
| font-size: 14.5px; | |
| font-weight: 500; | |
| border-radius: var(--radius-sm); | |
| transition: var(--transition-default); | |
| box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); | |
| } | |
| .auth-btn:hover { | |
| background: #1e293b; | |
| transform: translateY(-1px); | |
| } | |
| .auth-phase { | |
| display: none; | |
| } | |
| .auth-phase.active { | |
| display: block; | |
| } | |
| .auth-message { | |
| font-size: 13px; | |
| text-align: center; | |
| margin-top: 14px; | |
| min-height: 18px; | |
| font-weight: 500; | |
| } | |
| .mobile-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(15, 23, 42, 0.3); | |
| backdrop-filter: blur(3px); | |
| z-index: 95; | |
| opacity: 0; | |
| transition: 0.3s; | |
| pointer-events: none; | |
| } | |
| .mobile-overlay.active { | |
| display: block; | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| /* कस्टम कन्फर्म बॉक्स */ | |
| .custom-confirm-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(15, 23, 42, 0.4); | |
| backdrop-filter: blur(6px); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2500; | |
| } | |
| .custom-confirm-box { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(24px); | |
| -webkit-backdrop-filter: blur(24px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 28px; | |
| padding: 28px; | |
| max-width: 340px; | |
| width: 90%; | |
| text-align: center; | |
| box-shadow: 0 24px 48px rgba(0, 0, 0, 0.18); | |
| animation: modalFadeIn 0.2s ease; | |
| } | |
| .custom-confirm-msg { | |
| font-size: 16px; | |
| font-weight: 500; | |
| margin-bottom: 24px; | |
| color: var(--text-primary); | |
| line-height: 1.5; | |
| } | |
| .custom-confirm-btns { | |
| display: flex; | |
| gap: 12px; | |
| justify-content: center; | |
| } | |
| .custom-confirm-btn { | |
| flex: 1; | |
| padding: 12px; | |
| border-radius: 16px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| transition: var(--transition-default); | |
| } | |
| .custom-confirm-btn.cancel { | |
| background: rgba(240, 244, 249, 0.8); | |
| color: var(--text-secondary); | |
| } | |
| .custom-confirm-btn.confirm { | |
| background: var(--brand-danger); | |
| color: #fff; | |
| } | |
| .custom-confirm-btn.cancel:hover { | |
| background: rgba(220, 230, 240, 0.9); | |
| } | |
| .custom-confirm-btn.confirm:hover { | |
| background: #a01e18; | |
| } | |
| /* रिस्पॉन्सिव मीडिया क्वेरीज़ */ | |
| @media (max-width: 900px) { | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| box-shadow: 4px 0 32px rgba(0, 0, 0, 0.1); | |
| } | |
| .sidebar.collapsed { | |
| transform: translateX(-100%); | |
| box-shadow: none; | |
| } | |
| .input-dock { | |
| padding: 10px 10px 20px; | |
| } | |
| .input-container { | |
| border-radius: 28px; | |
| padding: 8px 10px 8px 16px; | |
| } | |
| .tool-btn { | |
| width: 38px; | |
| height: 38px; | |
| } | |
| .btn-send, .btn-stop { | |
| width: 40px; | |
| height: 40px; | |
| } | |
| .chat-container { | |
| padding: 60px 10px 160px; | |
| } | |
| .user-message { | |
| font-size: 14.5px; | |
| max-width: 90%; | |
| } | |
| .bot-content { | |
| font-size: 14.5px; | |
| } | |
| .welcome-center h1 { | |
| font-size: 28px; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .chat-container { | |
| padding: 50px 8px 140px; | |
| } | |
| .input-container { | |
| padding: 6px 8px 6px 12px; | |
| border-radius: 24px; | |
| } | |
| .tool-btn { | |
| width: 34px; | |
| height: 34px; | |
| } | |
| .btn-send, .btn-stop { | |
| width: 36px; | |
| height: 36px; | |
| } | |
| .chat-input { | |
| font-size: 14px; | |
| } | |
| .user-message { | |
| max-width: 92%; | |
| font-size: 14px; | |
| } | |
| .bot-content { | |
| font-size: 14px; | |
| } | |
| .welcome-center h1 { | |
| font-size: 26px; | |
| } | |
| } | |
| </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:36px; height:36px;" 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:18px; height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> | |
| </svg> | |
| New Chat | |
| </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;">Queries: 0/10</div> | |
| </div> | |
| <button class="btn-login-register" id="btnLoginRegister" onclick="Auth.openModal()">Login/Register</button> | |
| <button class="btn-power" id="btnDeleteAcc" onclick="Auth.handleDeleteAccount()" style="display:none; color:var(--brand-danger);" title="Delete Account"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path> | |
| </svg> | |
| </button> | |
| <button class="btn-power" id="btnLogout" onclick="Auth.handleLogout()" style="display:none;" title="Logout"> | |
| <svg style="width:18px;height:18px;" 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"> | |
| <div class="welcome-center" id="welcomeScreen"> | |
| <img src="https://i.ibb.co/MyYStcGP/TIRANGA-20260613-131924-0000.png" alt="CODE VED Logo"> | |
| <h1 id="welcomeTitle">Hello, Guest!<br>How can I help you today?</h1> | |
| <p id="welcomeGreeting" style="display: none;">Engineered by Divy Patel</p> | |
| </div> | |
| <div id="chatMessages"></div> | |
| </div> | |
| <button id="scrollToBottomBtn" class="btn-scroll-bottom" onclick="UI.scrollToBottom(true); UI.autoScroll = true;" title="Go to latest"> | |
| <svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <polyline points="19 12 12 19 5 12"></polyline> | |
| </svg> | |
| </button> | |
| <!-- इनपुट डोक --> | |
| <div class="input-dock" id="inputDock"> | |
| <div class="loc-toast" id="locStatus"></div> | |
| <div class="input-container"> | |
| <div class="file-preview-bar" id="filePreviewBar"> | |
| <div class="file-preview-pill"> | |
| <img id="imgPreview" class="file-preview-icon" src="" style="display:none;"> | |
| <svg id="docPreview" style="display:none; 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 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="3"> | |
| <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="input-row"> | |
| <div class="tools-left"> | |
| <div style="position: relative;"> | |
| <button class="tool-btn" onclick="UI.toggleAttachMenu()" title="Attach File"> | |
| <svg style="width:24px;height:24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| </button> | |
| <div class="dropdown-menu" id="attachMenu"> | |
| <div class="dropdown-header">Attachments</div> | |
| <div class="dropdown-item" onclick="document.getElementById('imgUpload').click()"> | |
| <span style="display:flex;align-items:center;gap:10px;"> | |
| <svg style="width:16px;height:16px;" 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 | |
| </span> | |
| </div> | |
| <div class="dropdown-item" onclick="document.getElementById('docUpload').click()"> | |
| <span style="display:flex;align-items:center;gap:10px;"> | |
| <svg style="width:16px;height:16px;" 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 | |
| </span> | |
| </div> | |
| <div style="height:1px; background:var(--border-light); margin:4px 0;"></div> | |
| <div class="dropdown-header">Tools</div> | |
| <div class="dropdown-item" onclick="EnvironmentManager.toggle()"> | |
| <span style="display:flex;align-items:center;gap:10px;"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <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> GPS & Weather | |
| </span> | |
| </div> | |
| <div class="dropdown-item" onclick="ThinkingManager.toggleMenu()"> | |
| <span style="display:flex;align-items:center;gap:10px;"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <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> Thinking Mode | |
| </span> | |
| </div> | |
| <div class="dropdown-item" onclick="TTSManager.toggleMenu()" style="color:#0b57d0;"> | |
| <span style="display:flex;align-items:center;gap:10px;"> | |
| <svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> | |
| <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> | |
| </svg> Select Voice Mode | |
| </span> | |
| </div> | |
| </div> | |
| <div class="dropdown-menu" id="voiceMenu" style="min-width: 220px; left: 45px; bottom: 40px; max-height: 300px; overflow-y: auto;"> | |
| <div class="dropdown-header">Premium Voices</div> | |
| <div id="voiceOptionsList"></div> | |
| <div style="height:1px; background:var(--border-light); margin:4px 0;"></div> | |
| <div class="dropdown-header">Language</div> | |
| <div id="languageOptionsList"></div> | |
| </div> | |
| <div class="dropdown-menu" id="thinkMenu" style="min-width: 180px; left: 45px; bottom: 40px;"> | |
| <div class="dropdown-header">Reasoning Effort</div> | |
| <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> | |
| <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> | |
| <button class="tool-btn" id="btnSearch" onclick="SearchManager.toggle()" title="Web Search: OFF"> | |
| <svg style="width:22px;height:22px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| <circle cx="11" cy="11" r="8"></circle> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <textarea class="chat-input" id="mainInput" placeholder="Ask CODE VED..." rows="1"></textarea> | |
| <button class="tool-btn btn-mic" id="btnStt" onclick="Speech.toggle()" title="Voice Typing"> | |
| <svg style="width:22px;height:22px;" 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> | |
| <button class="btn-stop" id="btnStop" onclick="Chat.stopGeneration()"> | |
| <svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="currentColor"> | |
| <rect x="6" y="6" width="12" height="12" rx="2"></rect> | |
| </svg> | |
| </button> | |
| <button class="btn-send" id="btnSend" onclick="Chat.handleSend()"> | |
| <svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> | |
| <line x1="12" y1="19" x2="12" y2="5"></line> | |
| <polyline points="5 12 12 5 19 12"></polyline> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- ऑथेंटिकेशन मोडल --> | |
| <div class="auth-overlay" id="authModal" onclick="if(event.target===this) Auth.closeModal()"> | |
| <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> | |
| <!-- कस्टम कन्फर्म बॉक्स --> | |
| <div class="custom-confirm-overlay" id="customConfirmOverlay"> | |
| <div class="custom-confirm-box"> | |
| <div class="custom-confirm-msg" id="customConfirmMsg"></div> | |
| <div class="custom-confirm-btns"> | |
| <button class="custom-confirm-btn cancel" id="customConfirmCancel">Cancel</button> | |
| <button class="custom-confirm-btn confirm" id="customConfirmOk">Confirm</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- मुख्य जावास्क्रिप्ट लॉजिक --> | |
| <script> | |
| (function() { | |
| // कॉन्फ़िगरेशन | |
| 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: (() => { | |
| const v = parseInt(localStorage.getItem('codeved_guest') || '0', 10); | |
| return isNaN(v) ? 0 : v; | |
| })(), | |
| attachment: null, | |
| isProcessing: false, | |
| history: [], | |
| currentThreadId: null, | |
| currentTitle: null, | |
| abortController: null, | |
| location: null, | |
| weatherContext: null, | |
| thinkingMode: false, | |
| thinkingEffort: "medium", | |
| searchEnabled: false, | |
| lastUserMessage: null | |
| }; | |
| // PDF.js वर्कर सेटअप | |
| if (typeof pdfjsLib !== 'undefined') { | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| } | |
| // कस्टम कन्फर्म बॉक्स फ़ंक्शन | |
| function customConfirm(message) { | |
| return new Promise((resolve) => { | |
| const overlay = document.getElementById('customConfirmOverlay'); | |
| const msgEl = document.getElementById('customConfirmMsg'); | |
| const cancelBtn = document.getElementById('customConfirmCancel'); | |
| const okBtn = document.getElementById('customConfirmOk'); | |
| msgEl.textContent = message; | |
| overlay.style.display = 'flex'; | |
| function cleanup() { | |
| overlay.style.display = 'none'; | |
| cancelBtn.removeEventListener('click', onCancel); | |
| okBtn.removeEventListener('click', onOk); | |
| } | |
| function onCancel() { cleanup(); resolve(false); } | |
| function onOk() { cleanup(); resolve(true); } | |
| cancelBtn.addEventListener('click', onCancel); | |
| okBtn.addEventListener('click', onOk); | |
| }); | |
| } | |
| // UI मैनेजमेंट | |
| const UI = { | |
| autoScroll: true, | |
| 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'); | |
| document.getElementById('voiceMenu').classList.remove('active'); | |
| }, | |
| autoGrow(el) { | |
| el.style.height = 'auto'; | |
| el.style.height = Math.min(el.scrollHeight, 180) + 'px'; | |
| }, | |
| scrollToBottom(force = false) { | |
| const c = document.getElementById('chatContainer'); | |
| if (force || this.autoScroll) { | |
| c.scrollTop = c.scrollHeight; | |
| } | |
| }, | |
| escape(s) { | |
| return String(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('btnLogout').style.display = 'flex'; | |
| document.getElementById('btnDeleteAcc').style.display = 'flex'; | |
| document.getElementById('btnLoginRegister').style.display = 'none'; | |
| document.getElementById('welcomeTitle').innerHTML = `Hello, ${dispName}!<br>How can I help you today?`; | |
| }, | |
| updateWelcomeScreen() { | |
| document.getElementById('welcomeScreen').style.display = State.history.length === 0 ? 'flex' : 'none'; | |
| } | |
| }; | |
| // स्क्रॉल इवेंट लिसनर | |
| const chatContainerEl = document.getElementById('chatContainer'); | |
| const scrollBtnEl = document.getElementById('scrollToBottomBtn'); | |
| chatContainerEl.addEventListener('scroll', () => { | |
| const scrollBottom = chatContainerEl.scrollHeight - chatContainerEl.scrollTop - chatContainerEl.clientHeight; | |
| UI.autoScroll = scrollBottom <= 50; | |
| scrollBtnEl.classList.toggle('visible', !UI.autoScroll); | |
| }); | |
| // बाहर क्लिक करने पर मेनू बंद करें | |
| document.addEventListener('click', (e) => { | |
| if (!e.target.closest('.tools-left')) { | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| document.getElementById('voiceMenu').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(); | |
| } | |
| }); | |
| // ऑथेंटिकेशन लॉजिक | |
| 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('btnDeleteAcc').style.display = 'none'; | |
| document.getElementById('btnLoginRegister').style.display = 'inline-flex'; | |
| document.getElementById('welcomeTitle').innerHTML = `Hello, Guest!<br>How can I help you today?`; | |
| } | |
| }, | |
| async handleLogout() { | |
| const ok = await customConfirm("Are you sure you want to logout?"); | |
| if (ok) { | |
| localStorage.removeItem('codeved_user'); | |
| localStorage.removeItem('codeved_name'); | |
| location.reload(); | |
| } | |
| }, | |
| async handleDeleteAccount() { | |
| const ok = await customConfirm("Are you sure you want to permanently delete your account and all data?"); | |
| if (ok) { | |
| try { | |
| await fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ action: "delete_account", email: State.user }) | |
| }); | |
| } catch(e) { | |
| console.error("Account delete error:", e); | |
| } finally { | |
| 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'); | |
| }, | |
| async process(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', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const textResponse = await res.text(); | |
| let data; | |
| try { | |
| data = JSON.parse(textResponse); | |
| } catch (parseErr) { | |
| console.error("Failed to parse GAS response:", textResponse); | |
| UI.showAuthMsg("Invalid response from server. Network error."); | |
| btn.disabled = false; | |
| btn.innerText = originalText; | |
| return; | |
| } | |
| 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); | |
| } | |
| location.reload(); | |
| } | |
| } else { | |
| UI.showAuthMsg(data.message); | |
| } | |
| } catch (e) { | |
| console.error("Auth process error:", e); | |
| UI.showAuthMsg("Network Error. Please try again."); | |
| } | |
| btn.disabled = false; | |
| btn.innerText = originalText; | |
| } | |
| }; | |
| // हिस्ट्री मैनेजमेंट | |
| const HistoryManager = { | |
| async syncAllChats() { | |
| if (!State.user) return; | |
| try { | |
| const res = await fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ action: "get_all_chats", email: State.user }) | |
| }); | |
| const textResponse = await res.text(); | |
| const data = JSON.parse(textResponse); | |
| if (data.status === 'success') { | |
| if (data.user) { | |
| UI.updateUserInfo(data.user.name, data.user.email); | |
| } | |
| this.renderSidebar(data.chats || []); | |
| } | |
| } catch (e) { | |
| console.error("History sync error:", e); | |
| } | |
| }, | |
| renderSidebar(chats) { | |
| const container = document.getElementById('chatHistory'); | |
| container.innerHTML = `<div class="history-header"><span class="history-title-text">Recent Workspaces</span><span class="btn-clear-all" onclick="HistoryManager.clearAll()">Clear All</span></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"> | |
| <svg style="width:14px;height:14px;" 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>`; | |
| }); | |
| }, | |
| async loadChat(threadId) { | |
| if (!State.user || State.isProcessing) return; | |
| State.currentThreadId = threadId; | |
| document.getElementById('chatMessages').innerHTML = ''; | |
| try { | |
| const res = await fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ action: "get_chat", email: State.user, threadId: threadId }) | |
| }); | |
| const textResponse = await res.text(); | |
| const data = JSON.parse(textResponse); | |
| if (data.status === 'success') { | |
| State.history = JSON.parse(data.historyJSON || "[]"); | |
| UI.updateWelcomeScreen(); | |
| for (const msg of State.history) { | |
| 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 msgId = 'hist_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); | |
| const botObj = Chat.renderBot(msgId); | |
| Chat.parseAndRender(msg.content, false, false, botObj.contentDiv); | |
| TTSManager.autoPrepare(msgId, msg.content, botObj.listenBtn); | |
| } | |
| } | |
| UI.autoScroll = true; | |
| UI.scrollToBottom(); | |
| if (window.innerWidth <= 900) UI.toggleSidebar(); | |
| } | |
| } catch (e) { | |
| console.error("Load chat error:", e); | |
| } | |
| }, | |
| startNew() { | |
| if (State.isProcessing) return; | |
| State.currentThreadId = null; | |
| State.currentTitle = null; | |
| State.history = []; | |
| document.getElementById('chatMessages').innerHTML = ''; | |
| UI.updateWelcomeScreen(); | |
| UI.autoScroll = true; | |
| UI.scrollToBottom(); | |
| 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'); | |
| State.currentTitle = firstUser ? firstUser.content.substring(0, 25) : "New Workspace"; | |
| } | |
| fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ | |
| action: "save_chat", | |
| email: State.user, | |
| threadId: State.currentThreadId, | |
| title: State.currentTitle, | |
| historyJSON: JSON.stringify(State.history) | |
| }) | |
| }).catch(e => console.error("Save error:", e)).then(() => this.syncAllChats()); | |
| }, | |
| async clearAll() { | |
| const ok = await customConfirm("Clear all workspaces?"); | |
| if (ok) { | |
| State.history = []; | |
| State.currentThreadId = null; | |
| State.currentTitle = null; | |
| document.getElementById('chatMessages').innerHTML = ''; | |
| UI.updateWelcomeScreen(); | |
| UI.autoScroll = true; | |
| UI.scrollToBottom(); | |
| fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ action: "clear_all_chats", email: State.user }) | |
| }).catch(e => console.error("Clear error:", e)).then(() => this.syncAllChats()); | |
| } | |
| }, | |
| async deleteChat(threadId) { | |
| const ok = await customConfirm("Delete this workspace?"); | |
| if (ok) { | |
| if (State.currentThreadId === threadId) this.startNew(); | |
| fetch(Config.GAS_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'text/plain;charset=utf-8' }, | |
| body: JSON.stringify({ action: "delete_chat", email: State.user, threadId: threadId }) | |
| }).catch(e => console.error("Delete error:", e)).then(() => this.syncAllChats()); | |
| } | |
| } | |
| }; | |
| const EnvironmentManager = { | |
| toggle() { | |
| const status = document.getElementById('locStatus'); | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| if (State.location) { | |
| State.location = null; | |
| State.weatherContext = null; | |
| status.style.display = 'none'; | |
| } else { | |
| if (navigator.geolocation) { | |
| status.style.display = 'flex'; | |
| status.innerHTML = `<span class="flicker-text">Finding location...</span>`; | |
| navigator.geolocation.getCurrentPosition(pos => { | |
| State.location = { lat: pos.coords.latitude, lng: pos.coords.longitude }; | |
| status.innerHTML = `<span style="color:var(--brand-success);">✓ Location active!</span>`; | |
| setTimeout(() => status.style.display = 'none', 3000); | |
| }, err => { | |
| status.innerHTML = `<span style="color:var(--brand-danger);">Location denied.</span>`; | |
| setTimeout(() => status.style.display = 'none', 3000); | |
| }); | |
| } | |
| } | |
| } | |
| }; | |
| const SearchManager = { | |
| toggle() { | |
| State.searchEnabled = !State.searchEnabled; | |
| const btn = document.getElementById('btnSearch'); | |
| btn.classList.toggle('active-search', State.searchEnabled); | |
| btn.title = State.searchEnabled ? 'Web Search: ON' : 'Web Search: OFF'; | |
| } | |
| }; | |
| const ThinkingManager = { | |
| toggleMenu() { | |
| document.getElementById('thinkMenu').classList.toggle('active'); | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| }, | |
| setEffort(level) { | |
| State.thinkingMode = true; | |
| State.thinkingEffort = level; | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| ThinkingManager.updateIndicator(); | |
| }, | |
| disable() { | |
| State.thinkingMode = false; | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| ThinkingManager.updateIndicator(); | |
| }, | |
| updateIndicator() { | |
| const btn = document.getElementById('btnSearch'); | |
| btn.classList.remove('active-think-low', 'active-think-medium', 'active-think-high'); | |
| if (State.thinkingMode) { | |
| btn.classList.add(`active-think-${State.thinkingEffort}`); | |
| } | |
| } | |
| }; | |
| const FileSys = { | |
| async process(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; | |
| if (type === 'image') { | |
| document.getElementById('imgPreview').style.display = 'block'; | |
| document.getElementById('docPreview').style.display = 'none'; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let w = img.width, h = img.height; | |
| const max = 1024; | |
| if(w > max || h > max) { | |
| if(w > h) { | |
| h = (max/w)*h; w = max; | |
| } else { | |
| w = (max/h)*w; h = max; | |
| } | |
| } | |
| canvas.width = w; | |
| canvas.height = h; | |
| ctx.drawImage(img, 0, 0, w, h); | |
| const b64 = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; | |
| State.attachment = { type: 'image', data: b64, name: file.name }; | |
| document.getElementById('imgPreview').src = `data:image/jpeg;base64,${b64}`; | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| document.getElementById('imgPreview').style.display = 'none'; | |
| document.getElementById('docPreview').style.display = 'block'; | |
| if (file.name.endsWith('.pdf')) { | |
| try { | |
| const arrayBuffer = await file.arrayBuffer(); | |
| const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; | |
| let text = ''; | |
| for (let i = 1; i <= pdf.numPages; i++) { | |
| const page = await pdf.getPage(i); | |
| const content = await page.getTextContent(); | |
| text += content.items.map(item => item.str).join(' ') + '\n'; | |
| } | |
| State.attachment = { type: 'text', data: text, name: file.name }; | |
| } catch(e) { | |
| State.attachment = { type: 'text', data: '[PDF parsing error]', name: file.name }; | |
| } | |
| } else if (file.name.endsWith('.docx')) { | |
| try { | |
| const arrayBuffer = await file.arrayBuffer(); | |
| const result = await mammoth.extractRawText({ arrayBuffer }); | |
| State.attachment = { type: 'text', data: result.value, name: file.name }; | |
| } catch(e) { | |
| State.attachment = { type: 'text', data: '[DOCX parsing error]', name: file.name }; | |
| } | |
| } else { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| State.attachment = { type: 'text', data: e.target.result, name: file.name }; | |
| }; | |
| reader.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; | |
| this.rec = new SR(); | |
| this.rec.continuous = false; | |
| this.rec.interimResults = true; | |
| this.rec.lang = navigator.language || 'en-US'; | |
| this.rec.onstart = () => { | |
| this.isRec = true; | |
| document.getElementById('btnStt').classList.add('recording'); | |
| }; | |
| this.rec.onresult = (e) => { | |
| if (!e.results || !e.results.length) return; | |
| let trans = ''; | |
| for (let i = e.resultIndex; i < e.results.length; ++i) { | |
| if (e.results[i].isFinal && e.results[i][0]) { | |
| trans += e.results[i][0].transcript; | |
| } | |
| } | |
| if (trans) { | |
| const input = document.getElementById('mainInput'); | |
| input.value += (input.value ? ' ' : '') + trans; | |
| UI.autoGrow(input); | |
| } | |
| }; | |
| this.rec.onerror = () => this.stop(); | |
| this.rec.onend = () => this.stop(); | |
| }, | |
| toggle() { | |
| if (!this.rec) this.init(); | |
| if (this.isRec) { | |
| this.stop(); | |
| } else { | |
| try { | |
| this.rec.start(); | |
| } catch(e) {} | |
| } | |
| }, | |
| stop() { | |
| if(this.rec) this.rec.stop(); | |
| this.isRec = false; | |
| document.getElementById('btnStt').classList.remove('recording'); | |
| } | |
| }; | |
| const renderer = { | |
| code(code, lang) { | |
| const language = (lang || '').match(/\S*/)?.[0]; | |
| if (language === 'mermaid') { | |
| return `<pre class="mermaid">${code}</pre>`; | |
| } | |
| if (language && hljs.getLanguage(language)) { | |
| try { | |
| return `<pre><code class="hljs ${language}">${hljs.highlight(code, { language }).value}</code></pre>`; | |
| } catch (__) {} | |
| } | |
| return `<pre><code class="hljs">${hljs.highlightAuto(code).value}</code></pre>`; | |
| } | |
| }; | |
| marked.use({ renderer, gfm: true, breaks: true, pedantic: false }); | |
| function initMermaid() { | |
| if (typeof mermaid !== 'undefined') { | |
| mermaid.initialize({ startOnLoad: false, theme: 'default' }); | |
| } | |
| } | |
| function renderMermaid(container) { | |
| if (typeof mermaid === 'undefined') return; | |
| const diagrams = container.querySelectorAll('.mermaid'); | |
| if (diagrams.length === 0) return; | |
| try { | |
| mermaid.run({ nodes: diagrams }); | |
| } catch (e) { | |
| console.warn('Mermaid render error:', e); | |
| } | |
| } | |
| function runRendering(container, isProcessing) { | |
| if (window.MathJax && !isProcessing) { | |
| try { | |
| MathJax.typesetClear([container]); | |
| MathJax.typesetPromise([container]).catch(console.error); | |
| } catch (e) {} | |
| } | |
| renderMermaid(container); | |
| } | |
| const TTSManager = { | |
| cache: {}, | |
| currentAudio: null, | |
| selectedVoice: "M2", | |
| selectedLanguage: "English", | |
| isPlaying: false, | |
| currentMsgId: null, | |
| currentBtnElement: null, | |
| preparingSet: new Set(), | |
| voiceMap: { | |
| "M1": "Aarav", "M2": "Kabir", "M3": "Vihaan", "M4": "Advik", "M5": "Rohan", | |
| "F1": "Priya", "F2": "Ananya", "F3": "Diya", "F4": "Sneha", "F5": "Kavya" | |
| }, | |
| languageMap: { | |
| "English": "en", "Korean": "ko", "Japanese": "ja", "Arabic": "ar", "Bulgarian": "bg", | |
| "Czech": "cs", "Danish": "da", "German": "de", "Greek": "el", "Spanish": "es", | |
| "Estonian": "et", "Finnish": "fi", "French": "fr", "Hindi": "hi", "Croatian": "hr", | |
| "Hungarian": "hu", "Indonesian": "id", "Italian": "it", "Lithuanian": "lt", "Latvian": "lv", | |
| "Dutch": "nl", "Polish": "pl", "Portuguese": "pt", "Romanian": "ro", "Russian": "ru", | |
| "Slovak": "sk", "Slovenian": "sl", "Swedish": "sv", "Turkish": "tr", "Ukrainian": "uk", | |
| "Vietnamese": "vi" | |
| }, | |
| initUI() { | |
| const list = document.getElementById('voiceOptionsList'); | |
| list.innerHTML = ''; | |
| for (const [code, name] of Object.entries(this.voiceMap)) { | |
| const isSelected = code === this.selectedVoice ? 'selected' : ''; | |
| const checkIcon = isSelected ? `<svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"></polyline></svg>` : `<div></div>`; | |
| list.innerHTML += `<div class="dropdown-item ${isSelected}" onclick="TTSManager.setVoice('${code}')"><span>${name}</span>${checkIcon}</div>`; | |
| } | |
| const langList = document.getElementById('languageOptionsList'); | |
| langList.innerHTML = ''; | |
| for (const [name, code] of Object.entries(this.languageMap)) { | |
| const isSelected = name === this.selectedLanguage ? 'selected' : ''; | |
| const checkIcon = isSelected ? `<svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"></polyline></svg>` : `<div></div>`; | |
| langList.innerHTML += `<div class="dropdown-item ${isSelected}" onclick="TTSManager.setLanguage('${name}')"><span>${name}</span>${checkIcon}</div>`; | |
| } | |
| }, | |
| toggleMenu() { | |
| document.getElementById('voiceMenu').classList.toggle('active'); | |
| document.getElementById('attachMenu').classList.remove('active'); | |
| document.getElementById('thinkMenu').classList.remove('active'); | |
| }, | |
| setVoice(code) { | |
| Object.values(this.cache).forEach(url => URL.revokeObjectURL(url)); | |
| this.cache = {}; | |
| this.selectedVoice = code; | |
| this.initUI(); | |
| document.getElementById('voiceMenu').classList.remove('active'); | |
| }, | |
| setLanguage(name) { | |
| Object.values(this.cache).forEach(url => URL.revokeObjectURL(url)); | |
| this.cache = {}; | |
| this.selectedLanguage = name; | |
| this.initUI(); | |
| document.getElementById('voiceMenu').classList.remove('active'); | |
| }, | |
| cleanTextForTTS(text) { | |
| let cleaned = text.replace(/\u0060{3}[\s\S]*?\u0060{3}/g, '') | |
| .replace(/\u0060[^\u0060]*\u0060/g, '') | |
| .replace(/<think>[\s\S]*?<\/think>/gi, '') | |
| .replace(/https?:\/\/[^\s]+/g, '') | |
| .replace(/[*_#\u0060~>]/g, '') | |
| .replace(/<[^>]*>/g, '') | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}]/gu, '') | |
| .replace(/[\u{1F300}-\u{1F5FF}]/gu, '') | |
| .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') | |
| .replace(/[\u{2600}-\u{26FF}]/gu, '') | |
| .replace(/[\u{2700}-\u{27BF}]/gu, ''); | |
| if (cleaned.length > 1500) { | |
| cleaned = cleaned.substring(0, 1500) + '...'; | |
| } | |
| return cleaned; | |
| }, | |
| async autoPrepare(msgId, fullText, btnElement) { | |
| if (this.preparingSet.has(msgId)) return; | |
| const cleanText = this.cleanTextForTTS(fullText); | |
| if (!cleanText) { | |
| btnElement.style.display = 'none'; | |
| return; | |
| } | |
| this.preparingSet.add(msgId); | |
| btnElement.style.display = 'flex'; | |
| btnElement.innerHTML = `<svg class="spin-icon" style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg> Preparing...`; | |
| btnElement.style.pointerEvents = 'none'; | |
| try { | |
| const response = await fetch('/api/tts', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ | |
| text: cleanText, | |
| voice: this.selectedVoice, | |
| language_name: this.selectedLanguage | |
| }) | |
| }); | |
| if (!response.ok) throw new Error('TTS service unavailable'); | |
| const blob = await response.blob(); | |
| this.cache[msgId] = URL.createObjectURL(blob); | |
| btnElement.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg> Listen`; | |
| btnElement.style.pointerEvents = 'auto'; | |
| } catch(e) { | |
| btnElement.innerHTML = `<span style="color:var(--brand-danger);">⚠️ ${e.message}</span>`; | |
| setTimeout(() => { btnElement.style.display = 'none'; }, 4000); | |
| } finally { | |
| this.preparingSet.delete(msgId); | |
| } | |
| }, | |
| play(msgId, btnElement) { | |
| const audioUrl = this.cache[msgId]; | |
| if (!audioUrl) { | |
| this.autoPrepare(msgId, document.getElementById(`bot-content-${msgId}`).innerText, btnElement); | |
| return; | |
| } | |
| if (this.isPlaying && this.currentMsgId === msgId) { | |
| if (this.currentAudio) { | |
| this.currentAudio.pause(); | |
| this.currentAudio.currentTime = 0; | |
| } | |
| this.isPlaying = false; | |
| this.currentMsgId = null; | |
| btnElement.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg> Listen`; | |
| btnElement.style.background = ''; | |
| return; | |
| } | |
| if (this.currentAudio) { | |
| this.currentAudio.pause(); | |
| this.currentAudio.currentTime = 0; | |
| } | |
| this.isPlaying = true; | |
| this.currentMsgId = msgId; | |
| this.currentBtnElement = btnElement; | |
| const originalHTML = btnElement.innerHTML; | |
| btnElement.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> Playing...`; | |
| btnElement.style.background = 'rgba(211, 227, 253, 0.9)'; | |
| this.currentAudio = new Audio(audioUrl); | |
| this.currentAudio.playbackRate = 1.15; | |
| this.currentAudio.onended = () => { | |
| this.isPlaying = false; | |
| this.currentMsgId = null; | |
| btnElement.innerHTML = originalHTML; | |
| btnElement.style.background = ''; | |
| }; | |
| this.currentAudio.onerror = () => { | |
| this.isPlaying = false; | |
| this.currentMsgId = null; | |
| btnElement.innerHTML = originalHTML; | |
| btnElement.style.background = ''; | |
| }; | |
| this.currentAudio.play(); | |
| } | |
| }; | |
| const Chat = { | |
| renderUser(txt, attachUI = '') { | |
| const c = document.getElementById('chatMessages'); | |
| 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.autoScroll = true; | |
| UI.scrollToBottom(true); | |
| }, | |
| renderBot(msgId) { | |
| const c = document.getElementById('chatMessages'); | |
| const w = document.createElement('div'); | |
| w.className = 'message-wrapper'; | |
| const listenIcon = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>`; | |
| 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>`; | |
| w.innerHTML = ` | |
| <div class="bot-message"> | |
| <div class="bot-avatar"><img src="${Config.LOGO}"></div> | |
| <div class="bot-content-wrapper"> | |
| <div class="bot-content" id="bot-content-${msgId}"></div> | |
| <div class="msg-actions"> | |
| <button class="action-btn listen-btn" id="listen-btn-${msgId}" onclick="TTSManager.play('${msgId}', this)" style="display:none;">${listenIcon} Listen</button> | |
| <button class="action-btn" onclick="Chat.copyMsg('${msgId}')">${copyIcon} Copy</button> | |
| </div> | |
| </div> | |
| </div>`; | |
| c.appendChild(w); | |
| UI.scrollToBottom(); | |
| return { | |
| contentDiv: document.getElementById(`bot-content-${msgId}`), | |
| listenBtn: document.getElementById(`listen-btn-${msgId}`) | |
| }; | |
| }, | |
| preprocessMath(text) { | |
| let mathBlocks = {}; | |
| let mathCounter = 0; | |
| function replacer(match) { | |
| // Changed placeholder to completely avoid underscores or any special markdown characters | |
| const id = `MATHBLOCKPLACEHOLDER${mathCounter}ENDPLACEHOLDER`; | |
| mathBlocks[id] = match; | |
| mathCounter++; | |
| return id; | |
| } | |
| text = text.replace(/\$\$([\s\S]+?)\$\$/g, replacer); | |
| text = text.replace(/\\\[([\s\S]+?)\\\]/g, replacer); | |
| text = text.replace(/\$((?:\\.|[^$\\])+?)\$/g, replacer); | |
| text = text.replace(/\\\(([\s\S]+?)\\\)/g, replacer); | |
| return { text, mathBlocks }; | |
| }, | |
| postprocessMath(html, mathBlocks) { | |
| for (const [id, mathStr] of Object.entries(mathBlocks)) { | |
| // Using split.join to ensure global replace regardless of markdown alterations | |
| html = html.split(id).join(mathStr); | |
| } | |
| return html; | |
| }, | |
| parseAndRender(fullText, isSearching, isProcessing, container) { | |
| 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); | |
| let finalHtml = ''; | |
| if (thinkMatch) { | |
| finalHtml += ` | |
| <details class="qwen-think-box" ${isProcessing ? 'open' : ''}> | |
| <summary> | |
| <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> | |
| Thinking Process | |
| </summary> | |
| <div class="qwen-think-content">${marked.parse(thinkMatch[1].trim())}</div> | |
| </details>`; | |
| } | |
| let mainText = normalizedText.replace(thinkRegex, '').trim(); | |
| if (mainText) { | |
| const { text: safeText, mathBlocks } = Chat.preprocessMath(mainText); | |
| let parsedHtml = marked.parse(safeText); | |
| parsedHtml = Chat.postprocessMath(parsedHtml, mathBlocks); | |
| finalHtml += `<div class="tex2jax_process">${parsedHtml}</div>`; | |
| } | |
| if (isProcessing) { | |
| finalHtml += `<span class="blinking-cursor"></span>`; | |
| } | |
| container.innerHTML = finalHtml; | |
| runRendering(container, isProcessing); | |
| }, | |
| copyMsg(id) { | |
| const el = document.getElementById(`bot-content-${id}`); | |
| const clone = el.cloneNode(true); | |
| const thinkBox = clone.querySelector('.qwen-think-box'); | |
| if (thinkBox) { | |
| thinkBox.remove(); | |
| } | |
| navigator.clipboard.writeText(clone.innerText).then(() => { | |
| const btn = el.parentElement.querySelector('.action-btn:not(.listen-btn)'); | |
| const original = btn.innerHTML; | |
| btn.innerHTML = `<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!`; | |
| setTimeout(() => btn.innerHTML = original, 1500); | |
| }); | |
| }, | |
| stopGeneration() { | |
| if (State.abortController) { | |
| State.abortController.abort(); | |
| State.abortController = null; | |
| } | |
| State.isProcessing = false; | |
| document.getElementById('btnSend').disabled = false; | |
| document.getElementById('btnSend').style.display = 'flex'; | |
| document.getElementById('btnStop').classList.remove('active'); | |
| }, | |
| async handleSend() { | |
| if(State.isProcessing) return; | |
| if (!State.user && State.guestCount >= 10) { | |
| Auth.openModal(); | |
| UI.showAuthMsg("Guest limit reached (10/10). Please login or register to continue.", true); | |
| return; | |
| } | |
| const input = document.getElementById('mainInput'); | |
| const text = input.value.trim(); | |
| if(!text && !State.attachment) return; | |
| if (!State.user) { | |
| let count = parseInt(localStorage.getItem('codeved_guest') || '0', 10); | |
| if (isNaN(count)) count = 0; | |
| count++; | |
| State.guestCount = count; | |
| localStorage.setItem('codeved_guest', count.toString()); | |
| document.getElementById('uSub').innerText = `Queries: ${count}/10`; | |
| } | |
| State.isProcessing = true; | |
| document.getElementById('btnSend').style.display = 'none'; | |
| document.getElementById('btnStop').classList.add('active'); | |
| State.lastUserMessage = text; | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| UI.updateWelcomeScreen(); | |
| let payloadStr = text, attachUI = '', 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-container"><div class="chat-file-pill"><svg style="width:14px;height:14px;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></svg> ${State.attachment.name}</div></div>`; | |
| } else { | |
| mediaArray.push(State.attachment); | |
| attachUI = `<div class="chat-attachment-container"><img src="data:image/jpeg;base64,${State.attachment.data}" class="chat-img-preview"></div>`; | |
| } | |
| } | |
| let systemInjectedPayload = payloadStr; | |
| if(State.weatherContext) { | |
| systemInjectedPayload = `${payloadStr}\n\n[SYSTEM REAL-TIME WEATHER: ${State.weatherContext}]`; | |
| } | |
| Chat.renderUser(text, attachUI); | |
| State.history.push({ role: 'user', content: payloadStr }); | |
| FileSys.discard(); | |
| const msgId = Date.now().toString(); | |
| const botObj = Chat.renderBot(msgId); | |
| Chat.parseAndRender("", false, true, botObj.contentDiv); | |
| State.abortController = new AbortController(); | |
| let fullText = ""; | |
| try { | |
| const res = await fetch(Config.API_ENDPOINT, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ | |
| message: systemInjectedPayload, | |
| attachments: mediaArray, | |
| is_search: State.searchEnabled, | |
| location: State.location, | |
| thinking_mode: State.thinkingMode, | |
| thinking_effort: State.thinkingEffort, | |
| history: State.history | |
| }), | |
| signal: State.abortController.signal | |
| }); | |
| if(!res.ok) { | |
| let errMsg = `Server error (${res.status})`; | |
| if (res.status === 404) { | |
| errMsg = 'Chat API endpoint not found (404). Please ensure the server is running.'; | |
| } else if (res.status === 500) { | |
| errMsg = 'Internal server error (500). Please try again later.'; | |
| } | |
| throw new Error(errMsg); | |
| } | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| while(true) { | |
| const {done, value} = await reader.read(); | |
| if(done) break; | |
| buffer += decoder.decode(value, {stream: true}); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| 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.error) { | |
| botObj.contentDiv.innerHTML += `<br><span style="color:var(--brand-danger);">⚠️ ${UI.escape(json.error)}</span>`; | |
| return; | |
| } | |
| if(json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content) { | |
| fullText += json.choices[0].delta.content; | |
| } | |
| } catch(e) {} | |
| Chat.parseAndRender(fullText, false, true, botObj.contentDiv); | |
| } | |
| } | |
| } | |
| Chat.parseAndRender(fullText, false, false, botObj.contentDiv); | |
| State.history.push({ role: 'assistant', content: fullText }); | |
| HistoryManager.saveCurrent(); | |
| } catch(e) { | |
| if (e.name === 'AbortError') { | |
| botObj.contentDiv.innerHTML += `<br><span style="color:var(--text-tertiary); font-style:italic;">⏹ Generation stopped.</span>`; | |
| if (fullText.trim()) { | |
| State.history.push({ role: 'assistant', content: fullText }); | |
| HistoryManager.saveCurrent(); | |
| } | |
| } else { | |
| botObj.contentDiv.innerHTML += `<br><span style="color:var(--brand-danger);">⚠️ ${UI.escape(e.message || 'Connection Offline.')}</span>`; | |
| } | |
| } finally { | |
| State.isProcessing = false; | |
| document.getElementById('btnSend').disabled = false; | |
| document.getElementById('btnSend').style.display = 'flex'; | |
| document.getElementById('btnStop').classList.remove('active'); | |
| if (UI.autoScroll) UI.scrollToBottom(); | |
| if(botObj.listenBtn && fullText.trim().length > 0) { | |
| TTSManager.autoPrepare(msgId, fullText, botObj.listenBtn); | |
| } | |
| } | |
| } | |
| }; | |
| // ग्लोबल स्कोप में एक्सपोज़ करें | |
| window.UI = UI; | |
| window.Auth = Auth; | |
| window.HistoryManager = HistoryManager; | |
| window.EnvironmentManager = EnvironmentManager; | |
| window.SearchManager = SearchManager; | |
| window.ThinkingManager = ThinkingManager; | |
| window.FileSys = FileSys; | |
| window.Speech = Speech; | |
| window.TTSManager = TTSManager; | |
| window.Chat = Chat; | |
| // मेरमेड इनिशियलाइज़ करें | |
| initMermaid(); | |
| // विंडो लोड होने पर इनिशियलाइज़ेशन | |
| window.onload = () => { | |
| Auth.init(); | |
| TTSManager.initUI(); | |
| UI.updateWelcomeScreen(); | |
| if(window.innerWidth > 900) { | |
| document.getElementById('sidebar').classList.remove('collapsed'); | |
| } | |
| }; | |
| })(); | |
| </script> | |
| </body> | |
| </html> |