Spaces:
Running
Running
Divy Patel commited on
Commit ·
4b87d79
0
Parent(s):
Clean deployment with comprehensive LFS tracking
Browse files- .gitattributes +7 -0
- .github/workflows/main.yml +47 -0
- Dockerfile +17 -0
- README.md +9 -0
- app.py +360 -0
- css/style.css +837 -0
- index.html +265 -0
- js/markdownRenderer.js +278 -0
- js/script.js +1221 -0
- logo.png +3 -0
- nginx.conf +27 -0
- requirements.txt +7 -0
.gitattributes
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.ttf filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.whl filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/main.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Space
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
sync-to-hf:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- name: Checkout Code
|
| 13 |
+
uses: actions/checkout@v3
|
| 14 |
+
|
| 15 |
+
- name: Clean History and Force Push to HF
|
| 16 |
+
env:
|
| 17 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 18 |
+
run: |
|
| 19 |
+
# 1. गिट कॉन्फ़िगरेशन
|
| 20 |
+
git config --global user.email "divy@vedaalabs.ai"
|
| 21 |
+
git config --global user.name "Divy Patel"
|
| 22 |
+
|
| 23 |
+
# 2. पुरानी हिस्ट्री को पूरी तरह से डिलीट करें और नई शुरुआत करें
|
| 24 |
+
rm -rf .git
|
| 25 |
+
git init
|
| 26 |
+
git branch -M main
|
| 27 |
+
|
| 28 |
+
# 3. LFS सेटअप और ट्रैकिंग (सभी बड़ी फाइलों के लिए)
|
| 29 |
+
git lfs install
|
| 30 |
+
git lfs track "*.ttf"
|
| 31 |
+
git lfs track "*.whl"
|
| 32 |
+
git lfs track "*.png"
|
| 33 |
+
git lfs track "*.jpg"
|
| 34 |
+
git lfs track "*.jpeg"
|
| 35 |
+
git lfs track "*.woff2"
|
| 36 |
+
git lfs track "*.wasm"
|
| 37 |
+
|
| 38 |
+
# 4. सभी फाइलों को नए सिरे से ऐड करें
|
| 39 |
+
git add .gitattributes
|
| 40 |
+
git add .
|
| 41 |
+
|
| 42 |
+
# 5. नया फ्रेश कमिट
|
| 43 |
+
git commit -m "Clean deployment with comprehensive LFS tracking"
|
| 44 |
+
|
| 45 |
+
# 6. हगिंग फेस रिमोट सेट करें और फोर्स पुश करें
|
| 46 |
+
git remote add hf https://user:${HF_TOKEN}@huggingface.co/spaces/Renderlib-dev/CodeVed
|
| 47 |
+
git push -f hf main
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official, ultra-lightweight Nginx image
|
| 2 |
+
FROM nginx:alpine
|
| 3 |
+
|
| 4 |
+
# Remove the default Nginx configuration file
|
| 5 |
+
RUN rm /etc/nginx/conf.d/default.conf
|
| 6 |
+
|
| 7 |
+
# Copy our custom Nginx configuration
|
| 8 |
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
| 9 |
+
|
| 10 |
+
# Copy all project files (HTML, CSS, JS, Images) to the Nginx public directory
|
| 11 |
+
COPY . /usr/share/nginx/html
|
| 12 |
+
|
| 13 |
+
# Hugging Face Spaces requires the app to listen on port 7860
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Start Nginx in the foreground (Required for Docker containers)
|
| 17 |
+
CMD ["nginx", "-g", "daemon off;"]
|
README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
title: Codeved
|
| 4 |
+
sdk: docker
|
| 5 |
+
emoji: 🚀
|
| 6 |
+
colorFrom: yellow
|
| 7 |
+
colorTo: pink
|
| 8 |
+
---
|
| 9 |
+
# Web-Vedika
|
app.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import io
|
| 6 |
+
import tempfile
|
| 7 |
+
from datetime import datetime, timedelta, timezone
|
| 8 |
+
from bs4 import BeautifulSoup
|
| 9 |
+
from flask import Flask, request, Response, stream_with_context, render_template_string
|
| 10 |
+
from supertonic import TTS
|
| 11 |
+
|
| 12 |
+
app = Flask(__name__)
|
| 13 |
+
|
| 14 |
+
# ----------------------------------------------------
|
| 15 |
+
# INIT RENDERLIB
|
| 16 |
+
# ----------------------------------------------------
|
| 17 |
+
print("Loading RenderLib...")
|
| 18 |
+
try:
|
| 19 |
+
from renderlib import RenderLib
|
| 20 |
+
renderer = RenderLib()
|
| 21 |
+
print("RenderLib loaded successfully!")
|
| 22 |
+
except ImportError as e:
|
| 23 |
+
print(f"RenderLib not installed yet or error: {e}")
|
| 24 |
+
renderer = None
|
| 25 |
+
|
| 26 |
+
# ----------------------------------------------------
|
| 27 |
+
# INIT TTS MODEL
|
| 28 |
+
# ----------------------------------------------------
|
| 29 |
+
print("Loading Supertonic TTS Model...")
|
| 30 |
+
try:
|
| 31 |
+
tts = TTS(auto_download=True)
|
| 32 |
+
print("TTS Model loaded successfully!")
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Error initializing TTS: {e}")
|
| 35 |
+
tts = None
|
| 36 |
+
|
| 37 |
+
VOICES = ["M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"]
|
| 38 |
+
LANGUAGES = {
|
| 39 |
+
"English": "en", "Korean": "ko", "Japanese": "ja", "Arabic": "ar",
|
| 40 |
+
"Bulgarian": "bg", "Czech": "cs", "Danish": "da", "German": "de",
|
| 41 |
+
"Greek": "el", "Spanish": "es", "Estonian": "et", "Finnish": "fi",
|
| 42 |
+
"French": "fr", "Hindi": "hi", "Croatian": "hr", "Hungarian": "hu",
|
| 43 |
+
"Indonesian": "id", "Italian": "it", "Lithuanian": "lt", "Latvian": "lv",
|
| 44 |
+
"Dutch": "nl", "Polish": "pl", "Portuguese": "pt", "Romanian": "ro",
|
| 45 |
+
"Russian": "ru", "Slovak": "sk", "Slovenian": "sl", "Swedish": "sv",
|
| 46 |
+
"Turkish": "tr", "Ukrainian": "uk", "Vietnamese": "vi"
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
VOICE_STYLES_CACHE = {}
|
| 50 |
+
|
| 51 |
+
# ----------------------------------------------------
|
| 52 |
+
# HARDCODED MODEL CONFIGURATION (Directly in code)
|
| 53 |
+
# ----------------------------------------------------
|
| 54 |
+
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "") # Secrets se le raha hai
|
| 55 |
+
# Agar API key bhi hardcode karna ho toh upar wali line ko hata kar yah likhein:
|
| 56 |
+
# NVIDIA_API_KEY = "your_actual_api_key_here"
|
| 57 |
+
|
| 58 |
+
INVOKE_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
|
| 59 |
+
MODEL_ID = "mistralai/mistral-small-4-119b-2603"
|
| 60 |
+
|
| 61 |
+
# ----------------------------------------------------
|
| 62 |
+
# GPS REVERSE GEOCODING
|
| 63 |
+
# ----------------------------------------------------
|
| 64 |
+
def get_address_from_coords(lat, lon):
|
| 65 |
+
try:
|
| 66 |
+
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}"
|
| 67 |
+
headers = {'User-Agent': 'CODE_VED_AI_System_by_Divy_Patel'}
|
| 68 |
+
response = requests.get(url, headers=headers, timeout=5)
|
| 69 |
+
data = response.json()
|
| 70 |
+
return data.get('display_name', f"Lat: {lat}, Lon: {lon}")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
return f"Lat: {lat}, Lon: {lon}"
|
| 73 |
+
|
| 74 |
+
# ----------------------------------------------------
|
| 75 |
+
# SERPAPI GOOGLE SEARCH ENGINE
|
| 76 |
+
# ----------------------------------------------------
|
| 77 |
+
def web_search_scraper(query, num_results=5, user_address=None):
|
| 78 |
+
results = []
|
| 79 |
+
serpapi_key = os.environ.get("SERPAPI_KEY")
|
| 80 |
+
if not serpapi_key:
|
| 81 |
+
return results
|
| 82 |
+
|
| 83 |
+
search_query = query
|
| 84 |
+
if user_address:
|
| 85 |
+
local_keywords = ["near", "nearby", "distance", "time", "where"]
|
| 86 |
+
if any(kw in query.lower() for kw in local_keywords):
|
| 87 |
+
search_query = f"{query} near {user_address}"
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
params = {"engine": "google", "q": search_query, "api_key": serpapi_key, "num": num_results, "hl": "en", "gl": "in"}
|
| 91 |
+
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
|
| 92 |
+
data = response.json()
|
| 93 |
+
|
| 94 |
+
if "organic_results" in data:
|
| 95 |
+
for item in data["organic_results"]:
|
| 96 |
+
title = item.get("title", "")
|
| 97 |
+
link = item.get("link", "")
|
| 98 |
+
snippet = item.get("snippet", "")
|
| 99 |
+
if title and snippet:
|
| 100 |
+
results.append({"title": title, "link": link, "snippet": snippet})
|
| 101 |
+
except Exception:
|
| 102 |
+
pass
|
| 103 |
+
return results
|
| 104 |
+
|
| 105 |
+
# ----------------------------------------------------
|
| 106 |
+
# RSS TECH NEWS SCRAPER
|
| 107 |
+
# ----------------------------------------------------
|
| 108 |
+
def get_live_web_data(query):
|
| 109 |
+
url = f"https://news.google.com/rss/search?q={query}&hl=en&gl=IN&ceid=IN:en"
|
| 110 |
+
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
| 111 |
+
tech_keywords = ["ai", "artificial intelligence", "smartphone", "mobile", "feature", "whatsapp", "google", "tech", "technology", "gadget", "apple", "nasa"]
|
| 112 |
+
block_keywords = ["share news", "stock"]
|
| 113 |
+
scraped_results = []
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
response = requests.get(url, headers=headers, timeout=6)
|
| 117 |
+
if response.status_code == 200:
|
| 118 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 119 |
+
items = soup.find_all('item')
|
| 120 |
+
for item in items:
|
| 121 |
+
title = item.title.text if item.title else "No Title"
|
| 122 |
+
title_lower = title.lower()
|
| 123 |
+
|
| 124 |
+
if any(b_kw in title_lower for b_kw in block_keywords):
|
| 125 |
+
continue
|
| 126 |
+
if any(t_kw in title_lower for t_kw in tech_keywords):
|
| 127 |
+
link = item.link.text if item.link else "#"
|
| 128 |
+
pub_date = item.pubdate.text if item.pubdate else ""
|
| 129 |
+
source = item.source.text if item.source else "Google News"
|
| 130 |
+
scraped_results.append({
|
| 131 |
+
"title": title,
|
| 132 |
+
"snippet": f"Published: {pub_date} | Source: {source}",
|
| 133 |
+
"link": link
|
| 134 |
+
})
|
| 135 |
+
if len(scraped_results) >= 5:
|
| 136 |
+
break
|
| 137 |
+
except Exception:
|
| 138 |
+
pass
|
| 139 |
+
return scraped_results
|
| 140 |
+
|
| 141 |
+
# ----------------------------------------------------
|
| 142 |
+
# HOME ROUTE
|
| 143 |
+
# ----------------------------------------------------
|
| 144 |
+
@app.route('/')
|
| 145 |
+
def home():
|
| 146 |
+
try:
|
| 147 |
+
with open('index.html', 'r', encoding='utf-8') as f:
|
| 148 |
+
return render_template_string(f.read())
|
| 149 |
+
except Exception as e:
|
| 150 |
+
return f"<h1>System Error</h1><p>index.html missing: {str(e)}</p>"
|
| 151 |
+
|
| 152 |
+
# ----------------------------------------------------
|
| 153 |
+
# RENDERLIB API ENDPOINT
|
| 154 |
+
# ----------------------------------------------------
|
| 155 |
+
@app.route('/api/render', methods=['POST'])
|
| 156 |
+
def render_content():
|
| 157 |
+
if renderer is None:
|
| 158 |
+
return Response(json.dumps({"error": "RenderLib is not available on the server."}), status=500, mimetype='application/json')
|
| 159 |
+
|
| 160 |
+
data = request.get_json() or {}
|
| 161 |
+
subject = data.get("subject", "math")
|
| 162 |
+
method = data.get("method", "to_latex")
|
| 163 |
+
args = data.get("args", [])
|
| 164 |
+
kwargs = data.get("kwargs", {})
|
| 165 |
+
|
| 166 |
+
expression = data.get("expression")
|
| 167 |
+
if expression and not args:
|
| 168 |
+
args = [expression]
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
result = renderer.render(subject, method, *args, **kwargs)
|
| 172 |
+
return Response(json.dumps({"result": result}), mimetype='application/json')
|
| 173 |
+
except Exception as e:
|
| 174 |
+
return Response(json.dumps({"error": f"RenderLib Error: {str(e)}"}), status=500, mimetype='application/json')
|
| 175 |
+
|
| 176 |
+
# ----------------------------------------------------
|
| 177 |
+
# CHAT API ENDPOINT (Hardcoded Model & URL)
|
| 178 |
+
# ----------------------------------------------------
|
| 179 |
+
@app.route('/api/chat', methods=['POST'])
|
| 180 |
+
def chat():
|
| 181 |
+
if not NVIDIA_API_KEY:
|
| 182 |
+
return Response(json.dumps({"error": "Configuration Error: NVIDIA_API_KEY is missing."}), mimetype='application/json', status=500)
|
| 183 |
+
|
| 184 |
+
data = request.get_json() or {}
|
| 185 |
+
user_message = data.get("message", "")
|
| 186 |
+
attachments = data.get("attachments", [])
|
| 187 |
+
is_search = data.get("is_search", False)
|
| 188 |
+
history = data.get("history", [])
|
| 189 |
+
location = data.get("location")
|
| 190 |
+
user_address = None
|
| 191 |
+
max_tokens = data.get("max_tokens", 4096)
|
| 192 |
+
|
| 193 |
+
ist_time = datetime.now(timezone.utc) + timedelta(hours=5, minutes=30)
|
| 194 |
+
current_date = ist_time.strftime("%A, %d %B %Y, %I:%M %p IST")
|
| 195 |
+
|
| 196 |
+
thinking_mode = data.get("thinking_mode", False)
|
| 197 |
+
thinking_effort = data.get("thinking_effort", "medium")
|
| 198 |
+
|
| 199 |
+
thinking_instruction = ""
|
| 200 |
+
if thinking_mode:
|
| 201 |
+
thinking_instruction = f"\n[CRITICAL INSTRUCTION: THINKING MODE ENABLED - Effort: {thinking_effort}]\nYou MUST format your reasoning exactly inside <think> and </think> HTML tags."
|
| 202 |
+
|
| 203 |
+
location_instruction = ""
|
| 204 |
+
if location and location.get('lat') and location.get('lng'):
|
| 205 |
+
user_address = get_address_from_coords(location['lat'], location['lng'])
|
| 206 |
+
location_instruction = f"\n[USER REAL-TIME LOCATION: {user_address}]"
|
| 207 |
+
|
| 208 |
+
system_prompt = f"""[CRITICAL IDENTITY OVERRIDE]
|
| 209 |
+
Name: CODE VED
|
| 210 |
+
Creator/Engineer: Divy Patel
|
| 211 |
+
Current Time: {current_date}.{location_instruction}{thinking_instruction}
|
| 212 |
+
|
| 213 |
+
You are "Code Ved," an expert AI software engineering and technical consultant. Your goal is to provide precise, clean, and highly optimized code solutions, architectural advice, and technical explanations.
|
| 214 |
+
Operational Guidelines:
|
| 215 |
+
Technical Accuracy: Provide code that follows industry best practices, is secure, and includes necessary comments for clarity.
|
| 216 |
+
Efficiency: Prioritize performance, scalability, and maintainability in all architectural suggestions.
|
| 217 |
+
Clarity & Structure: Break down complex problems into logical steps. Use code blocks for snippets and markdown tables for comparing technical approaches.
|
| 218 |
+
Debugging Mindset: When provided with errors, analyze the root cause before offering the fix, and explain why the solution works.
|
| 219 |
+
Language & Tone: Maintain a professional, objective, and helpful tone. Be direct and concise, avoiding unnecessary fluff.
|
| 220 |
+
Constraints:
|
| 221 |
+
Always provide context-aware code; if multiple languages or frameworks are applicable, suggest the best fit with reasoning.
|
| 222 |
+
Ensure all code snippets are complete, syntactically correct, and follow the latest stable versions of the requested technologies.
|
| 223 |
+
If a user request is ambiguous, ask for necessary technical specifications before proceeding to ensure the output meets the requirements.
|
| 224 |
+
Formatting Standards:
|
| 225 |
+
Use standard Markdown for all responses.
|
| 226 |
+
Use LaTeX for any mathematical notations or algorithmic complexity analysis (e.g., Big O notation).
|
| 227 |
+
For complex architectural patterns, describe the flow clearly using structured lists.
|
| 228 |
+
"""
|
| 229 |
+
|
| 230 |
+
if is_search:
|
| 231 |
+
search_context = ""
|
| 232 |
+
scraped_data = web_search_scraper(user_message, user_address=user_address)
|
| 233 |
+
news_data = get_live_web_data(user_message)
|
| 234 |
+
if scraped_data:
|
| 235 |
+
search_context += "\n\n--- [LIVE GOOGLE SEARCH] ---\n"
|
| 236 |
+
for idx, res in enumerate(scraped_data):
|
| 237 |
+
search_context += f"{idx+1}. {res['title']}: {res['snippet']} (URL: {res['link']})\n"
|
| 238 |
+
if news_data:
|
| 239 |
+
search_context += "\n--- [LIVE TECH NEWS] ---\n"
|
| 240 |
+
for idx, res in enumerate(news_data):
|
| 241 |
+
search_context += f"{idx+1}. {res['title']}: {res['snippet']} (URL: {res['link']})\n"
|
| 242 |
+
if search_context:
|
| 243 |
+
user_message = f"{user_message}\n{search_context}\n[COMMAND: Base your final answer strictly on the facts provided above.]"
|
| 244 |
+
|
| 245 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 246 |
+
|
| 247 |
+
for msg in history:
|
| 248 |
+
if msg == history[-1] and msg.get("role") == "user":
|
| 249 |
+
continue
|
| 250 |
+
role = msg.get("role", "user")
|
| 251 |
+
if role not in ["system", "user", "assistant"]:
|
| 252 |
+
role = "user"
|
| 253 |
+
content = msg.get("content", "")
|
| 254 |
+
if isinstance(content, list):
|
| 255 |
+
text_parts = [item["text"] for item in content if item.get("type") == "text"]
|
| 256 |
+
content = " ".join(text_parts)
|
| 257 |
+
if "Gemma" in content or "DeepMind" in content or "Google" in content:
|
| 258 |
+
continue
|
| 259 |
+
if content:
|
| 260 |
+
messages.append({"role": role, "content": str(content)})
|
| 261 |
+
|
| 262 |
+
if attachments:
|
| 263 |
+
content_payload = [{"type": "text", "text": user_message}]
|
| 264 |
+
for att in attachments:
|
| 265 |
+
att_type = att.get("type")
|
| 266 |
+
b64_data = att.get("data")
|
| 267 |
+
if att_type == "image":
|
| 268 |
+
content_payload.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_data}"}})
|
| 269 |
+
messages.append({"role": "user", "content": content_payload})
|
| 270 |
+
else:
|
| 271 |
+
messages.append({"role": "user", "content": user_message})
|
| 272 |
+
|
| 273 |
+
headers = {
|
| 274 |
+
"Authorization": f"Bearer {NVIDIA_API_KEY}",
|
| 275 |
+
"Accept": "text/event-stream",
|
| 276 |
+
"Content-Type": "application/json"
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
payload = {
|
| 280 |
+
"model": MODEL_ID,
|
| 281 |
+
"messages": messages,
|
| 282 |
+
"max_tokens": 128000,
|
| 283 |
+
"temperature": 1.0,
|
| 284 |
+
"top_p": 0.95,
|
| 285 |
+
"stream": True
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
try:
|
| 289 |
+
response = requests.post(INVOKE_URL, headers=headers, json=payload, stream=True, timeout=60)
|
| 290 |
+
if response.status_code != 200:
|
| 291 |
+
err_text = response.text[:200]
|
| 292 |
+
err_msg = json.dumps({"error": f"API Error {response.status_code}: {err_text}"})
|
| 293 |
+
return Response(f"data: {err_msg}\n\n", mimetype='text/event-stream')
|
| 294 |
+
|
| 295 |
+
def generate():
|
| 296 |
+
for line in response.iter_lines():
|
| 297 |
+
if line:
|
| 298 |
+
decoded = line.decode("utf-8")
|
| 299 |
+
if decoded.startswith("data: ") and "[DONE]" not in decoded:
|
| 300 |
+
try:
|
| 301 |
+
data_json = json.loads(decoded[6:])
|
| 302 |
+
if "choices" in data_json and len(data_json["choices"]) > 0:
|
| 303 |
+
delta = data_json["choices"][0].get("delta", {})
|
| 304 |
+
if "content" in delta and delta["content"]:
|
| 305 |
+
content = delta["content"]
|
| 306 |
+
content = content.replace("<|channel|>thought <|channel|>", "<think>\n")
|
| 307 |
+
content = content.replace("<|channel|>answer <|channel|>", "\n</think>\n")
|
| 308 |
+
delta["content"] = content
|
| 309 |
+
yield "data: " + json.dumps(data_json) + "\n\n"
|
| 310 |
+
except Exception:
|
| 311 |
+
yield decoded + "\n\n"
|
| 312 |
+
else:
|
| 313 |
+
yield decoded + "\n\n"
|
| 314 |
+
|
| 315 |
+
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
| 316 |
+
except Exception as e:
|
| 317 |
+
err_msg = json.dumps({"error": str(e)})
|
| 318 |
+
return Response(f"data: {err_msg}\n\n", mimetype='text/event-stream')
|
| 319 |
+
|
| 320 |
+
# ----------------------------------------------------
|
| 321 |
+
# TTS DIRECT API ENDPOINT
|
| 322 |
+
# ----------------------------------------------------
|
| 323 |
+
@app.route('/api/tts', methods=['POST'])
|
| 324 |
+
def generate_tts():
|
| 325 |
+
if tts is None:
|
| 326 |
+
return Response(json.dumps({"error": "TTS model failed to load on server."}), status=500, mimetype='application/json')
|
| 327 |
+
|
| 328 |
+
data = request.get_json() or {}
|
| 329 |
+
text = data.get("text", "")
|
| 330 |
+
voice = data.get("voice", "M2")
|
| 331 |
+
language_name = data.get("language_name", "English")
|
| 332 |
+
|
| 333 |
+
if not text.strip():
|
| 334 |
+
return Response(json.dumps({"error": "Text is empty."}), status=400, mimetype='application/json')
|
| 335 |
+
|
| 336 |
+
try:
|
| 337 |
+
lang_code = LANGUAGES.get(language_name, "en")
|
| 338 |
+
if voice not in VOICE_STYLES_CACHE:
|
| 339 |
+
VOICE_STYLES_CACHE[voice] = tts.get_voice_style(voice_name=voice)
|
| 340 |
+
style = VOICE_STYLES_CACHE[voice]
|
| 341 |
+
if style is None:
|
| 342 |
+
return Response(json.dumps({"error": f"Voice '{voice}' not available."}), status=400, mimetype='application/json')
|
| 343 |
+
|
| 344 |
+
wav, duration = tts.synthesize(text, voice_style=style, lang=lang_code)
|
| 345 |
+
|
| 346 |
+
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
|
| 347 |
+
tmp_path = tmp_file.name
|
| 348 |
+
tts.save_audio(wav, tmp_path)
|
| 349 |
+
|
| 350 |
+
with open(tmp_path, 'rb') as f:
|
| 351 |
+
audio_data = f.read()
|
| 352 |
+
os.unlink(tmp_path)
|
| 353 |
+
|
| 354 |
+
return Response(audio_data, mimetype="audio/wav")
|
| 355 |
+
except Exception as e:
|
| 356 |
+
print(f"TTS Synthesis Error: {e}")
|
| 357 |
+
return Response(json.dumps({"error": f"TTS synthesis failed: {str(e)}"}), status=500, mimetype='application/json')
|
| 358 |
+
|
| 359 |
+
if __name__ == '__main__':
|
| 360 |
+
app.run(host='0.0.0.0', port=7860)
|
css/style.css
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* ================================================================================
|
| 3 |
+
* CODE VED - GLOBAL STYLES & VARIABLES
|
| 4 |
+
* Anthropic-like Clean, Professional & Responsive Design
|
| 5 |
+
* ================================================================================
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
:root {
|
| 9 |
+
/* Backgrounds */
|
| 10 |
+
--bg-main: #ffffff;
|
| 11 |
+
--bg-sidebar: rgba(249, 250, 251, 0.92);
|
| 12 |
+
--bg-user-msg: #f4f4f5;
|
| 13 |
+
|
| 14 |
+
/* Text Colors */
|
| 15 |
+
--text-primary: #18181b;
|
| 16 |
+
--text-secondary: #52525b;
|
| 17 |
+
--text-tertiary: #a1a1aa;
|
| 18 |
+
|
| 19 |
+
/* Borders & Shadows */
|
| 20 |
+
--border-light: rgba(0, 0, 0, 0.08);
|
| 21 |
+
--border-focus: rgba(0, 0, 0, 0.15);
|
| 22 |
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04);
|
| 23 |
+
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
|
| 24 |
+
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.08);
|
| 25 |
+
|
| 26 |
+
/* Brand Colors */
|
| 27 |
+
--brand-color: #18181b;
|
| 28 |
+
--brand-accent: #0f9e78;
|
| 29 |
+
--brand-danger: #ef4444;
|
| 30 |
+
--brand-warning: #f59e0b;
|
| 31 |
+
--brand-success: #10b981;
|
| 32 |
+
|
| 33 |
+
/* Typography */
|
| 34 |
+
--font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 35 |
+
--font-code: 'JetBrains Mono', monospace;
|
| 36 |
+
|
| 37 |
+
/* Radii */
|
| 38 |
+
--radius-sm: 8px;
|
| 39 |
+
--radius-md: 12px;
|
| 40 |
+
--radius-lg: 20px;
|
| 41 |
+
|
| 42 |
+
/* Transitions */
|
| 43 |
+
--transition-default: 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/*
|
| 47 |
+
* ================================================================================
|
| 48 |
+
* GLOBAL RESET & BASE STYLES
|
| 49 |
+
* ================================================================================
|
| 50 |
+
*/
|
| 51 |
+
* {
|
| 52 |
+
box-sizing: border-box;
|
| 53 |
+
margin: 0;
|
| 54 |
+
padding: 0;
|
| 55 |
+
-webkit-user-select: none;
|
| 56 |
+
-moz-user-select: none;
|
| 57 |
+
-ms-user-select: none;
|
| 58 |
+
user-select: none;
|
| 59 |
+
-webkit-touch-callout: none;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Allow selection only in inputs and code */
|
| 63 |
+
input, textarea, [contenteditable="true"], code, pre {
|
| 64 |
+
-webkit-user-select: auto !important;
|
| 65 |
+
-moz-user-select: auto !important;
|
| 66 |
+
-ms-user-select: auto !important;
|
| 67 |
+
user-select: auto !important;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
*:focus {
|
| 71 |
+
outline: none !important;
|
| 72 |
+
box-shadow: none !important;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
html, body {
|
| 76 |
+
height: 100%;
|
| 77 |
+
height: 100dvh;
|
| 78 |
+
width: 100vw;
|
| 79 |
+
font-family: var(--font-ui);
|
| 80 |
+
color: var(--text-primary);
|
| 81 |
+
display: flex;
|
| 82 |
+
overflow: hidden;
|
| 83 |
+
background: var(--bg-main);
|
| 84 |
+
-webkit-font-smoothing: antialiased;
|
| 85 |
+
-moz-osx-font-smoothing: grayscale;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
button, a {
|
| 89 |
+
-webkit-tap-highlight-color: transparent;
|
| 90 |
+
outline: none;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
button {
|
| 94 |
+
background: none;
|
| 95 |
+
border: none;
|
| 96 |
+
cursor: pointer;
|
| 97 |
+
color: inherit;
|
| 98 |
+
font-family: inherit;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
input, textarea {
|
| 102 |
+
font-family: inherit;
|
| 103 |
+
outline: none;
|
| 104 |
+
border: none;
|
| 105 |
+
background: transparent;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Custom Scrollbar */
|
| 109 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 110 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 111 |
+
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 10px; }
|
| 112 |
+
::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
|
| 113 |
+
|
| 114 |
+
/*
|
| 115 |
+
* ================================================================================
|
| 116 |
+
* KEYFRAME ANIMATIONS
|
| 117 |
+
* ================================================================================
|
| 118 |
+
*/
|
| 119 |
+
@keyframes fadeInUp {
|
| 120 |
+
from { opacity: 0; transform: translateY(12px); }
|
| 121 |
+
to { opacity: 1; transform: translateY(0); }
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@keyframes dropUpFade {
|
| 125 |
+
from { opacity: 0; transform: translateY(10px) scale(0.96); }
|
| 126 |
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes modalFadeIn {
|
| 130 |
+
from { opacity: 0; transform: scale(0.96); }
|
| 131 |
+
to { opacity: 1; transform: scale(1); }
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
@keyframes blinkCursor {
|
| 135 |
+
0%, 100% { opacity: 1; }
|
| 136 |
+
50% { opacity: 0; }
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@keyframes recordPulse {
|
| 140 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
| 141 |
+
50% { transform: scale(1.05); box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
|
| 142 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.blinking-cursor {
|
| 146 |
+
display: inline-block;
|
| 147 |
+
width: 6px;
|
| 148 |
+
height: 18px;
|
| 149 |
+
background: var(--text-primary);
|
| 150 |
+
margin-left: 2px;
|
| 151 |
+
vertical-align: text-bottom;
|
| 152 |
+
animation: blinkCursor 0.8s infinite;
|
| 153 |
+
border-radius: 2px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/*
|
| 157 |
+
* ================================================================================
|
| 158 |
+
* SIDEBAR & NAVIGATION
|
| 159 |
+
* ================================================================================
|
| 160 |
+
*/
|
| 161 |
+
.btn-menu {
|
| 162 |
+
position: absolute;
|
| 163 |
+
top: 16px;
|
| 164 |
+
left: 16px;
|
| 165 |
+
z-index: 100;
|
| 166 |
+
width: 40px;
|
| 167 |
+
height: 40px;
|
| 168 |
+
border-radius: 50%;
|
| 169 |
+
display: flex;
|
| 170 |
+
align-items: center;
|
| 171 |
+
justify-content: center;
|
| 172 |
+
background: rgba(255,255,255,0.8);
|
| 173 |
+
backdrop-filter: blur(12px);
|
| 174 |
+
-webkit-backdrop-filter: blur(12px);
|
| 175 |
+
border: 1px solid var(--border-light);
|
| 176 |
+
color: var(--text-secondary);
|
| 177 |
+
transition: var(--transition-default);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.btn-menu:hover {
|
| 181 |
+
background: #fff;
|
| 182 |
+
color: var(--text-primary);
|
| 183 |
+
border-color: var(--border-focus);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.sidebar {
|
| 187 |
+
width: 280px;
|
| 188 |
+
background: var(--bg-sidebar);
|
| 189 |
+
backdrop-filter: blur(20px);
|
| 190 |
+
-webkit-backdrop-filter: blur(20px);
|
| 191 |
+
border-right: 1px solid var(--border-light);
|
| 192 |
+
display: flex;
|
| 193 |
+
flex-direction: column;
|
| 194 |
+
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 195 |
+
z-index: 110;
|
| 196 |
+
height: 100%;
|
| 197 |
+
flex-shrink: 0;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.sidebar.collapsed {
|
| 201 |
+
transform: translateX(-100%);
|
| 202 |
+
position: absolute;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.sidebar-header {
|
| 206 |
+
padding: 20px 16px 10px;
|
| 207 |
+
display: flex;
|
| 208 |
+
align-items: center;
|
| 209 |
+
justify-content: space-between;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.sidebar-logo {
|
| 213 |
+
font-weight: 600;
|
| 214 |
+
font-size: 15px;
|
| 215 |
+
display: flex;
|
| 216 |
+
align-items: center;
|
| 217 |
+
gap: 8px;
|
| 218 |
+
letter-spacing: -0.3px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.btn-new-chat {
|
| 222 |
+
margin: 10px 16px;
|
| 223 |
+
display: flex;
|
| 224 |
+
align-items: center;
|
| 225 |
+
justify-content: center;
|
| 226 |
+
gap: 8px;
|
| 227 |
+
padding: 10px;
|
| 228 |
+
border-radius: var(--radius-sm);
|
| 229 |
+
background: #fff;
|
| 230 |
+
border: 1px solid var(--border-light);
|
| 231 |
+
font-size: 13.5px;
|
| 232 |
+
font-weight: 500;
|
| 233 |
+
transition: var(--transition-default);
|
| 234 |
+
color: var(--text-primary);
|
| 235 |
+
box-shadow: var(--shadow-sm);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.btn-new-chat:hover {
|
| 239 |
+
border-color: var(--border-focus);
|
| 240 |
+
box-shadow: var(--shadow-md);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.sidebar-history {
|
| 244 |
+
flex: 1;
|
| 245 |
+
overflow-y: auto;
|
| 246 |
+
padding: 10px 12px;
|
| 247 |
+
display: flex;
|
| 248 |
+
flex-direction: column;
|
| 249 |
+
gap: 2px;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.history-header {
|
| 253 |
+
display: flex;
|
| 254 |
+
justify-content: space-between;
|
| 255 |
+
align-items: center;
|
| 256 |
+
padding: 4px 8px 8px;
|
| 257 |
+
margin-bottom: 4px;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.history-title-text {
|
| 261 |
+
font-size: 11px;
|
| 262 |
+
font-weight: 600;
|
| 263 |
+
color: var(--text-tertiary);
|
| 264 |
+
text-transform: uppercase;
|
| 265 |
+
letter-spacing: 0.5px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.btn-clear-all {
|
| 269 |
+
font-size: 11px;
|
| 270 |
+
font-weight: 500;
|
| 271 |
+
color: var(--text-tertiary);
|
| 272 |
+
cursor: pointer;
|
| 273 |
+
transition: 0.2s;
|
| 274 |
+
padding: 4px 8px;
|
| 275 |
+
border-radius: 6px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.btn-clear-all:hover {
|
| 279 |
+
color: var(--brand-danger);
|
| 280 |
+
background: rgba(239, 68, 68, 0.05);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.history-item {
|
| 284 |
+
padding: 9px 10px;
|
| 285 |
+
border-radius: var(--radius-sm);
|
| 286 |
+
font-size: 13px;
|
| 287 |
+
font-weight: 400;
|
| 288 |
+
color: var(--text-secondary);
|
| 289 |
+
cursor: pointer;
|
| 290 |
+
display: flex;
|
| 291 |
+
justify-content: space-between;
|
| 292 |
+
align-items: center;
|
| 293 |
+
transition: 0.15s;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.history-item:hover {
|
| 297 |
+
background: rgba(0,0,0,0.04);
|
| 298 |
+
color: var(--text-primary);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.history-item.active {
|
| 302 |
+
background: #fff;
|
| 303 |
+
color: var(--text-primary);
|
| 304 |
+
font-weight: 500;
|
| 305 |
+
box-shadow: var(--shadow-sm);
|
| 306 |
+
border: 1px solid var(--border-light);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.history-text {
|
| 310 |
+
white-space: nowrap;
|
| 311 |
+
overflow: hidden;
|
| 312 |
+
text-overflow: ellipsis;
|
| 313 |
+
flex: 1;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.btn-delete-chat {
|
| 317 |
+
background: none;
|
| 318 |
+
border: none;
|
| 319 |
+
color: var(--text-tertiary);
|
| 320 |
+
cursor: pointer;
|
| 321 |
+
padding: 4px;
|
| 322 |
+
display: none;
|
| 323 |
+
border-radius: 6px;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.history-item:hover .btn-delete-chat {
|
| 327 |
+
display: flex;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.btn-delete-chat:hover {
|
| 331 |
+
color: var(--brand-danger);
|
| 332 |
+
background: #fee2e2;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.sidebar-footer {
|
| 336 |
+
padding: 16px;
|
| 337 |
+
border-top: 1px solid var(--border-light);
|
| 338 |
+
display: flex;
|
| 339 |
+
align-items: center;
|
| 340 |
+
gap: 10px;
|
| 341 |
+
flex-wrap: wrap;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.user-avatar {
|
| 345 |
+
width: 34px;
|
| 346 |
+
height: 34px;
|
| 347 |
+
border-radius: 50%;
|
| 348 |
+
background: var(--text-primary);
|
| 349 |
+
color: #fff;
|
| 350 |
+
display: flex;
|
| 351 |
+
align-items: center;
|
| 352 |
+
justify-content: center;
|
| 353 |
+
font-size: 13px;
|
| 354 |
+
font-weight: 600;
|
| 355 |
+
flex-shrink: 0;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.user-info-box {
|
| 359 |
+
flex: 1;
|
| 360 |
+
overflow: hidden;
|
| 361 |
+
display: flex;
|
| 362 |
+
flex-direction: column;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.user-name {
|
| 366 |
+
font-size: 13px;
|
| 367 |
+
font-weight: 600;
|
| 368 |
+
white-space: nowrap;
|
| 369 |
+
overflow: hidden;
|
| 370 |
+
text-overflow: ellipsis;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.user-sub {
|
| 374 |
+
font-size: 11px;
|
| 375 |
+
color: var(--text-tertiary);
|
| 376 |
+
margin-top: 2px;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.btn-login-register {
|
| 380 |
+
background: var(--brand-color);
|
| 381 |
+
color: #fff;
|
| 382 |
+
border-radius: var(--radius-sm);
|
| 383 |
+
padding: 7px 12px;
|
| 384 |
+
font-size: 12px;
|
| 385 |
+
font-weight: 500;
|
| 386 |
+
cursor: pointer;
|
| 387 |
+
transition: 0.2s;
|
| 388 |
+
margin-left: auto;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.btn-login-register:hover {
|
| 392 |
+
background: #000;
|
| 393 |
+
transform: translateY(-1px);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.btn-power {
|
| 397 |
+
width: 32px;
|
| 398 |
+
height: 32px;
|
| 399 |
+
border-radius: var(--radius-sm);
|
| 400 |
+
display: flex;
|
| 401 |
+
align-items: center;
|
| 402 |
+
justify-content: center;
|
| 403 |
+
color: var(--text-secondary);
|
| 404 |
+
transition: 0.2s;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.btn-power:hover {
|
| 408 |
+
background: #fee2e2;
|
| 409 |
+
color: var(--brand-danger);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
/*
|
| 413 |
+
* ================================================================================
|
| 414 |
+
* MAIN CHAT AREA
|
| 415 |
+
* ================================================================================
|
| 416 |
+
*/
|
| 417 |
+
.main-area {
|
| 418 |
+
flex: 1;
|
| 419 |
+
display: flex;
|
| 420 |
+
flex-direction: column;
|
| 421 |
+
position: relative;
|
| 422 |
+
transition: 0.3s;
|
| 423 |
+
height: 100%;
|
| 424 |
+
overflow: hidden;
|
| 425 |
+
z-index: 1;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.chat-container {
|
| 429 |
+
flex: 1;
|
| 430 |
+
overflow-x: hidden;
|
| 431 |
+
overflow-y: auto;
|
| 432 |
+
padding: 80px 20px 200px;
|
| 433 |
+
display: flex;
|
| 434 |
+
flex-direction: column;
|
| 435 |
+
align-items: center;
|
| 436 |
+
scroll-behavior: smooth;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.welcome-center {
|
| 440 |
+
margin: auto;
|
| 441 |
+
text-align: center;
|
| 442 |
+
display: flex;
|
| 443 |
+
flex-direction: column;
|
| 444 |
+
align-items: center;
|
| 445 |
+
animation: fadeInUp 0.6s ease;
|
| 446 |
+
width: 100%;
|
| 447 |
+
justify-content: center;
|
| 448 |
+
flex: 1;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.welcome-center img {
|
| 452 |
+
width: 64px;
|
| 453 |
+
height: 64px;
|
| 454 |
+
border-radius: 16px;
|
| 455 |
+
margin-bottom: 24px;
|
| 456 |
+
object-fit: contain;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.welcome-center h1 {
|
| 460 |
+
font-size: 28px;
|
| 461 |
+
font-weight: 500;
|
| 462 |
+
letter-spacing: -0.5px;
|
| 463 |
+
color: var(--text-primary);
|
| 464 |
+
margin-bottom: 8px;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.welcome-center p {
|
| 468 |
+
font-size: 15px;
|
| 469 |
+
color: var(--text-secondary);
|
| 470 |
+
font-weight: 400;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
#chatMessages { width: 100%; display: flex; flex-direction: column; align-items: center; }
|
| 474 |
+
|
| 475 |
+
.message-wrapper {
|
| 476 |
+
width: 100%;
|
| 477 |
+
max-width: 800px;
|
| 478 |
+
margin-bottom: 32px;
|
| 479 |
+
display: flex;
|
| 480 |
+
flex-direction: column;
|
| 481 |
+
animation: fadeInUp 0.3s ease;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
/* User Message */
|
| 485 |
+
.user-message {
|
| 486 |
+
align-self: flex-end;
|
| 487 |
+
max-width: 85%;
|
| 488 |
+
background: var(--bg-user-msg);
|
| 489 |
+
padding: 12px 18px;
|
| 490 |
+
border-radius: 18px 18px 4px 18px;
|
| 491 |
+
font-size: 15px;
|
| 492 |
+
font-weight: 400;
|
| 493 |
+
line-height: 1.5;
|
| 494 |
+
color: var(--text-primary);
|
| 495 |
+
word-wrap: break-word;
|
| 496 |
+
overflow-wrap: break-word;
|
| 497 |
+
border: 1px solid rgba(0,0,0,0.04);
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/* Bot Message */
|
| 501 |
+
.bot-message {
|
| 502 |
+
align-self: flex-start;
|
| 503 |
+
max-width: 100%;
|
| 504 |
+
display: flex;
|
| 505 |
+
gap: 16px;
|
| 506 |
+
width: 100%;
|
| 507 |
+
box-sizing: border-box;
|
| 508 |
+
background: transparent;
|
| 509 |
+
padding: 0;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.bot-avatar {
|
| 513 |
+
width: 30px;
|
| 514 |
+
height: 30px;
|
| 515 |
+
border-radius: 8px;
|
| 516 |
+
flex-shrink: 0;
|
| 517 |
+
overflow: hidden;
|
| 518 |
+
display: flex;
|
| 519 |
+
align-items: center;
|
| 520 |
+
justify-content: center;
|
| 521 |
+
margin-top: 4px;
|
| 522 |
+
border: 1px solid var(--border-light);
|
| 523 |
+
background: #fff;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.bot-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
| 527 |
+
|
| 528 |
+
.bot-content-wrapper {
|
| 529 |
+
flex: 1 1 0%;
|
| 530 |
+
min-width: 0;
|
| 531 |
+
max-width: calc(100% - 46px);
|
| 532 |
+
width: 100%;
|
| 533 |
+
overflow: hidden;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.bot-content {
|
| 537 |
+
font-size: 15.5px;
|
| 538 |
+
line-height: 1.7;
|
| 539 |
+
color: var(--text-primary);
|
| 540 |
+
word-wrap: break-word;
|
| 541 |
+
font-weight: 400;
|
| 542 |
+
letter-spacing: 0.1px;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.bot-content p { margin-bottom: 16px; }
|
| 546 |
+
.bot-content p:last-child { margin-bottom: 0; }
|
| 547 |
+
.bot-content strong { font-weight: 600; color: #000; }
|
| 548 |
+
.bot-content ul, .bot-content ol { padding-left: 24px; margin-bottom: 16px; }
|
| 549 |
+
.bot-content li { margin-bottom: 8px; }
|
| 550 |
+
|
| 551 |
+
.bot-content code {
|
| 552 |
+
font-family: var(--font-code);
|
| 553 |
+
background: rgba(0,0,0,0.06);
|
| 554 |
+
padding: 3px 6px;
|
| 555 |
+
border-radius: 6px;
|
| 556 |
+
font-size: 0.85em;
|
| 557 |
+
color: #000;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.bot-content pre {
|
| 561 |
+
background: #0d1117;
|
| 562 |
+
padding: 16px;
|
| 563 |
+
border-radius: var(--radius-md);
|
| 564 |
+
overflow-x: auto;
|
| 565 |
+
margin: 16px 0;
|
| 566 |
+
border: 1px solid var(--border-light);
|
| 567 |
+
position: relative;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.bot-content pre code {
|
| 571 |
+
background: none;
|
| 572 |
+
padding: 0;
|
| 573 |
+
color: #e2e8f0;
|
| 574 |
+
font-size: 13.5px;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.bot-content table {
|
| 578 |
+
width: 100%;
|
| 579 |
+
border-collapse: collapse;
|
| 580 |
+
margin: 16px 0;
|
| 581 |
+
font-size: 14px;
|
| 582 |
+
border: 1px solid var(--border-light);
|
| 583 |
+
border-radius: 8px;
|
| 584 |
+
overflow: hidden;
|
| 585 |
+
display: block;
|
| 586 |
+
overflow-x: auto;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.bot-content th, .bot-content td {
|
| 590 |
+
border: 1px solid var(--border-light);
|
| 591 |
+
padding: 10px 14px;
|
| 592 |
+
text-align: left;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.bot-content th {
|
| 596 |
+
background: rgba(0,0,0,0.03);
|
| 597 |
+
font-weight: 500;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.mjx-container { max-width: 100%; overflow-x: auto; overflow-y: hidden; }
|
| 601 |
+
|
| 602 |
+
/* Attachments */
|
| 603 |
+
.chat-attachment-container { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
| 604 |
+
.chat-file-pill { display: inline-flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--border-light); padding: 8px 12px; border-radius: 10px; font-size: 12.5px; font-weight: 500; }
|
| 605 |
+
.chat-img-preview { max-width: 140px; max-height: 140px; border-radius: var(--radius-md); object-fit: cover; border: 1px solid var(--border-light); }
|
| 606 |
+
|
| 607 |
+
/* Thinking Box */
|
| 608 |
+
.qwen-think-box { border-left: 2px solid #e4e4e7; padding-left: 16px; margin-bottom: 16px; transition: 0.3s; }
|
| 609 |
+
.qwen-think-box summary { font-size: 12.5px; font-weight: 500; color: var(--text-tertiary); cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; user-select: none; }
|
| 610 |
+
.qwen-think-box summary::-webkit-details-marker { display: none; }
|
| 611 |
+
.qwen-think-content { margin-top: 8px; font-size: 14px; color: var(--text-secondary); font-style: italic; white-space: pre-wrap; line-height: 1.6; }
|
| 612 |
+
|
| 613 |
+
/* Message Actions */
|
| 614 |
+
.msg-actions { display: flex; gap: 4px; margin-top: 12px; opacity: 0; transition: 0.2s; align-items: center; }
|
| 615 |
+
.message-wrapper:hover .msg-actions { opacity: 1; }
|
| 616 |
+
@media (hover: none) { .msg-actions { opacity: 1; } }
|
| 617 |
+
|
| 618 |
+
.action-btn { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 500; color: var(--text-tertiary); padding: 6px 10px; border-radius: 8px; transition: 0.2s; background: transparent; }
|
| 619 |
+
.action-btn:hover { color: var(--text-primary); background: rgba(0,0,0,0.04); }
|
| 620 |
+
.action-btn.listen-btn { background: #f4f4f5; }
|
| 621 |
+
.action-btn.listen-btn:hover { background: #e4e4e7; }
|
| 622 |
+
|
| 623 |
+
.code-copy-btn { position: absolute; top: 8px; right: 8px; display: flex; align-items: center; gap: 4px; padding: 4px 8px; font-size: 11px; color: var(--text-secondary); background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; cursor: pointer; z-index: 2; backdrop-filter: blur(4px); }
|
| 624 |
+
.code-copy-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
|
| 625 |
+
.code-copy-btn.copied { color: var(--brand-success); border-color: var(--brand-success); }
|
| 626 |
+
|
| 627 |
+
.btn-scroll-bottom {
|
| 628 |
+
position: absolute;
|
| 629 |
+
bottom: 120px;
|
| 630 |
+
left: 50%;
|
| 631 |
+
transform: translateX(-50%) translateY(20px);
|
| 632 |
+
width: 36px;
|
| 633 |
+
height: 36px;
|
| 634 |
+
border-radius: 50%;
|
| 635 |
+
background: #fff;
|
| 636 |
+
border: 1px solid var(--border-light);
|
| 637 |
+
box-shadow: var(--shadow-md);
|
| 638 |
+
display: flex;
|
| 639 |
+
align-items: center;
|
| 640 |
+
justify-content: center;
|
| 641 |
+
color: var(--text-secondary);
|
| 642 |
+
opacity: 0;
|
| 643 |
+
pointer-events: none;
|
| 644 |
+
transition: 0.3s;
|
| 645 |
+
z-index: 45;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.btn-scroll-bottom.visible {
|
| 649 |
+
opacity: 1;
|
| 650 |
+
pointer-events: auto;
|
| 651 |
+
transform: translateX(-50%) translateY(0);
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
/*
|
| 655 |
+
* ================================================================================
|
| 656 |
+
* INPUT DOCK (The Core UI - Clean & Professional)
|
| 657 |
+
* ================================================================================
|
| 658 |
+
*/
|
| 659 |
+
.input-dock {
|
| 660 |
+
position: absolute;
|
| 661 |
+
bottom: 24px;
|
| 662 |
+
left: 50%;
|
| 663 |
+
transform: translateX(-50%);
|
| 664 |
+
width: 100%;
|
| 665 |
+
max-width: 800px;
|
| 666 |
+
padding: 0 20px;
|
| 667 |
+
z-index: 50;
|
| 668 |
+
display: flex;
|
| 669 |
+
flex-direction: column;
|
| 670 |
+
align-items: center;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.loc-toast { display: none; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 12px; background: #fff; padding: 6px 14px; border-radius: 16px; border: 1px solid var(--border-light); box-shadow: var(--shadow-md); }
|
| 674 |
+
|
| 675 |
+
.input-dock-wrapper {
|
| 676 |
+
position: relative;
|
| 677 |
+
width: 100%;
|
| 678 |
+
background: rgba(255, 255, 255, 0.85);
|
| 679 |
+
backdrop-filter: blur(20px);
|
| 680 |
+
-webkit-backdrop-filter: blur(20px);
|
| 681 |
+
border: 1px solid var(--border-light);
|
| 682 |
+
border-radius: var(--radius-lg);
|
| 683 |
+
padding: 12px 16px;
|
| 684 |
+
display: flex;
|
| 685 |
+
flex-direction: column;
|
| 686 |
+
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 687 |
+
box-shadow: var(--shadow-md);
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
/* Expanded State (No RGB Glow, just crisp shadow & border) */
|
| 691 |
+
.input-dock-wrapper.expanded {
|
| 692 |
+
background: rgba(255, 255, 255, 0.98);
|
| 693 |
+
border-color: var(--border-focus);
|
| 694 |
+
transform: translateY(-2px);
|
| 695 |
+
box-shadow: var(--shadow-lg);
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.file-preview-bar { display: none; padding-bottom: 10px; align-items: center; gap: 8px; }
|
| 699 |
+
.file-preview-bar.active { display: flex; }
|
| 700 |
+
.file-preview-pill { display: flex; align-items: center; gap: 8px; background: #f4f4f5; padding: 6px 12px; border-radius: 10px; font-size: 12px; border: 1px solid var(--border-light); }
|
| 701 |
+
.file-preview-icon { width: 16px; height: 16px; object-fit: cover; border-radius: 4px; }
|
| 702 |
+
.file-preview-name { max-width: 120px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-primary); }
|
| 703 |
+
.btn-remove-file { width: 18px; height: 18px; border-radius: 50%; background: #e4e4e7; display: flex; align-items: center; justify-content: center; transition: 0.2s; }
|
| 704 |
+
.btn-remove-file:hover { background: #d4d4d8; }
|
| 705 |
+
|
| 706 |
+
.chat-input {
|
| 707 |
+
width: 100%;
|
| 708 |
+
padding: 8px 4px 12px;
|
| 709 |
+
font-size: 15px;
|
| 710 |
+
font-weight: 400;
|
| 711 |
+
resize: none;
|
| 712 |
+
color: var(--text-primary);
|
| 713 |
+
line-height: 1.5;
|
| 714 |
+
background: transparent;
|
| 715 |
+
caret-color: #000;
|
| 716 |
+
min-height: 24px;
|
| 717 |
+
max-height: 200px;
|
| 718 |
+
transition: min-height 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.chat-input::placeholder { color: var(--text-tertiary); font-weight: 400; }
|
| 722 |
+
|
| 723 |
+
.input-dock-wrapper.expanded .chat-input {
|
| 724 |
+
min-height: 60px;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.input-row { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-top: 4px; }
|
| 728 |
+
|
| 729 |
+
.tools-left { display: flex; gap: 2px; align-items: center; position: relative; }
|
| 730 |
+
|
| 731 |
+
.tool-btn { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); transition: 0.2s; background: transparent; flex-shrink: 0; }
|
| 732 |
+
.tool-btn:hover { background: #f4f4f5; color: var(--text-primary); }
|
| 733 |
+
|
| 734 |
+
/* Tool Active States */
|
| 735 |
+
.tool-btn.active-env { color: #fff; background: var(--brand-accent); }
|
| 736 |
+
.tool-btn.active-think-low { color: #fff; background: var(--brand-danger); }
|
| 737 |
+
.tool-btn.active-think-medium { color: #fff; background: var(--brand-warning); }
|
| 738 |
+
.tool-btn.active-think-high { color: #fff; background: #8b5cf6; }
|
| 739 |
+
.tool-btn.active-search { color: #fff; background: #16803c; }
|
| 740 |
+
|
| 741 |
+
.btn-mic.recording { color: var(--brand-danger); background: #fee2e2; animation: recordPulse 1.5s infinite; }
|
| 742 |
+
|
| 743 |
+
.tools-right { display: flex; align-items: center; gap: 8px; }
|
| 744 |
+
|
| 745 |
+
.btn-send { width: 34px; height: 34px; border-radius: 50%; background: var(--brand-color); color: #fff; display: flex; align-items: center; justify-content: center; transition: 0.2s; flex-shrink: 0; }
|
| 746 |
+
.btn-send:hover:not(:disabled) { background: #000; transform: scale(1.05); }
|
| 747 |
+
.btn-send:disabled { background: #e4e4e7; color: #a1a1aa; cursor: not-allowed; }
|
| 748 |
+
|
| 749 |
+
.btn-stop { width: 34px; height: 34px; border-radius: 50%; background: #fee2e2; color: var(--brand-danger); display: none; align-items: center; justify-content: center; transition: 0.25s; flex-shrink: 0; }
|
| 750 |
+
.btn-stop.active { display: flex; }
|
| 751 |
+
.btn-stop:hover { background: #fecaca; }
|
| 752 |
+
|
| 753 |
+
.hidden-input { display: none; }
|
| 754 |
+
|
| 755 |
+
/* Dropdowns */
|
| 756 |
+
.dropdown-menu {
|
| 757 |
+
position: absolute;
|
| 758 |
+
bottom: calc(100% + 12px);
|
| 759 |
+
left: 0;
|
| 760 |
+
background: rgba(255, 255, 255, 0.98);
|
| 761 |
+
backdrop-filter: blur(20px);
|
| 762 |
+
border: 1px solid var(--border-light);
|
| 763 |
+
border-radius: 14px;
|
| 764 |
+
padding: 6px;
|
| 765 |
+
box-shadow: var(--shadow-lg);
|
| 766 |
+
display: none;
|
| 767 |
+
flex-direction: column;
|
| 768 |
+
min-width: 200px;
|
| 769 |
+
z-index: 100;
|
| 770 |
+
transform-origin: bottom left;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.dropdown-menu.active { display: flex; animation: dropUpFade 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
| 774 |
+
|
| 775 |
+
.dropdown-header { padding: 8px 12px; font-size: 11px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
| 776 |
+
|
| 777 |
+
.dropdown-item { padding: 9px 12px; border-radius: 8px; font-size: 13px; display: flex; align-items: center; justify-content: space-between; gap: 8px; cursor: pointer; transition: 0.15s; color: var(--text-primary); }
|
| 778 |
+
.dropdown-item:hover { background: #f4f4f5; }
|
| 779 |
+
.dropdown-item.selected { background: #f4f4f5; font-weight: 500; }
|
| 780 |
+
|
| 781 |
+
/*
|
| 782 |
+
* ================================================================================
|
| 783 |
+
* MODALS & OVERLAYS
|
| 784 |
+
* ================================================================================
|
| 785 |
+
*/
|
| 786 |
+
.mobile-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.2); backdrop-filter: blur(4px); z-index: 95; opacity: 0; transition: 0.3s ease; pointer-events: none; }
|
| 787 |
+
.mobile-overlay.active { display: block; opacity: 1; pointer-events: all; }
|
| 788 |
+
|
| 789 |
+
.auth-overlay { position: fixed; inset: 0; background: rgba(0,0,0, 0.3); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 2000; }
|
| 790 |
+
.auth-modal { background: #ffffff; width: 90%; max-width: 360px; border-radius: 20px; padding: 32px; box-shadow: 0 20px 50px rgba(0,0,0, 0.15); position: relative; animation: modalFadeIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); border: 1px solid var(--border-light); }
|
| 791 |
+
.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%; background: #f4f4f5; transition: 0.2s; }
|
| 792 |
+
.auth-close:hover { background: #e4e4e7; color: var(--text-primary); }
|
| 793 |
+
.auth-title { font-size: 18px; font-weight: 600; margin-bottom: 24px; text-align: center; letter-spacing: -0.3px; }
|
| 794 |
+
.auth-tabs { display: flex; background: #f4f4f5; border-radius: 10px; padding: 4px; margin-bottom: 20px; }
|
| 795 |
+
.auth-tab { flex: 1; text-align: center; padding: 8px; font-size: 13px; font-weight: 500; cursor: pointer; border-radius: 8px; color: var(--text-secondary); transition: 0.2s; }
|
| 796 |
+
.auth-tab.active { background: #ffffff; color: #000; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
| 797 |
+
.auth-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border-light); border-radius: 10px; font-size: 14px; margin-bottom: 12px; background: #fcfcfc; transition: 0.2s; }
|
| 798 |
+
.auth-input:focus { border-color: var(--text-primary); background: #fff; }
|
| 799 |
+
.auth-btn { width: 100%; padding: 12px; background: var(--brand-color); color: #fff; font-size: 14px; font-weight: 500; border-radius: 10px; transition: 0.2s; }
|
| 800 |
+
.auth-btn:hover { background: #000; }
|
| 801 |
+
.auth-phase { display: none; }
|
| 802 |
+
.auth-phase.active { display: block; }
|
| 803 |
+
.auth-message { font-size: 12px; text-align: center; margin-top: 12px; min-height: 16px; }
|
| 804 |
+
|
| 805 |
+
.custom-confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 2500; }
|
| 806 |
+
.custom-confirm-box { background: #fff; border-radius: 16px; padding: 24px; max-width: 300px; width: 90%; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.15); animation: modalFadeIn 0.2s; border: 1px solid var(--border-light); }
|
| 807 |
+
.custom-confirm-msg { font-size: 14px; margin-bottom: 24px; color: var(--text-primary); font-weight: 500; line-height: 1.5; }
|
| 808 |
+
.custom-confirm-btns { display: flex; gap: 12px; }
|
| 809 |
+
.custom-confirm-btn { flex: 1; padding: 10px; border-radius: 10px; font-size: 13px; font-weight: 500; transition: 0.2s; }
|
| 810 |
+
.custom-confirm-btn.cancel { background: #f4f4f5; color: var(--text-secondary); }
|
| 811 |
+
.custom-confirm-btn.cancel:hover { background: #e4e4e7; }
|
| 812 |
+
.custom-confirm-btn.confirm { background: var(--brand-danger); color: #fff; }
|
| 813 |
+
.custom-confirm-btn.confirm:hover { background: #dc2626; }
|
| 814 |
+
|
| 815 |
+
/*
|
| 816 |
+
* ================================================================================
|
| 817 |
+
* RESPONSIVE DESIGN
|
| 818 |
+
* ================================================================================
|
| 819 |
+
*/
|
| 820 |
+
@media (max-width: 900px) {
|
| 821 |
+
.sidebar { position: fixed; left: 0; top: 0; box-shadow: 4px 0 24px rgba(0,0,0,0.05); }
|
| 822 |
+
.sidebar.collapsed { box-shadow: none; }
|
| 823 |
+
.input-dock { padding: 10px 16px calc(16px + env(safe-area-inset-bottom)); bottom: 0; }
|
| 824 |
+
.chat-container { padding: 70px 16px 180px; }
|
| 825 |
+
.user-message { max-width: 90%; }
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
@media (max-width: 600px) {
|
| 829 |
+
.code-copy-btn span { display: none; }
|
| 830 |
+
.welcome-center img { width: 56px; height: 56px; }
|
| 831 |
+
.welcome-center h1 { font-size: 24px; }
|
| 832 |
+
.tool-btn, .btn-send, .btn-stop { width: 36px; height: 36px; }
|
| 833 |
+
.input-dock-wrapper.expanded .chat-input { min-height: 50px; }
|
| 834 |
+
|
| 835 |
+
/* Prevents iOS zoom on focus */
|
| 836 |
+
.chat-input { font-size: 16px; }
|
| 837 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 6 |
+
<title>CODE VED | Engineered by Divy Patel</title>
|
| 7 |
+
|
| 8 |
+
<!-- Favicon -->
|
| 9 |
+
<link rel="icon" type="image/webp" id="faviconLink">
|
| 10 |
+
|
| 11 |
+
<!-- Google Fonts -->
|
| 12 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 13 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 14 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 15 |
+
|
| 16 |
+
<!-- External Libraries CSS -->
|
| 17 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
| 18 |
+
|
| 19 |
+
<!-- Custom CSS (Placeholder for next step) -->
|
| 20 |
+
<link rel="stylesheet" href="css/style.css">
|
| 21 |
+
</head>
|
| 22 |
+
|
| 23 |
+
<body oncontextmenu="return false;">
|
| 24 |
+
|
| 25 |
+
<!-- Top Left Menu Button -->
|
| 26 |
+
<button class="btn-menu" id="btnMenuOpen">
|
| 27 |
+
<svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="14" y2="15"></line></svg>
|
| 28 |
+
</button>
|
| 29 |
+
|
| 30 |
+
<!-- Mobile Overlay for Sidebar -->
|
| 31 |
+
<div class="mobile-overlay" id="mobileOverlay"></div>
|
| 32 |
+
|
| 33 |
+
<!-- Sidebar -->
|
| 34 |
+
<aside class="sidebar collapsed" id="sidebar">
|
| 35 |
+
<div class="sidebar-header">
|
| 36 |
+
<div class="sidebar-logo">
|
| 37 |
+
<img id="sidebarLogoImg" alt="CODE VED" style="width:24px;height:24px;border-radius:6px;object-fit:contain;">
|
| 38 |
+
<span>CODE VED</span>
|
| 39 |
+
</div>
|
| 40 |
+
<button class="btn-menu btn-close-sidebar" id="btnCloseSidebar" style="position:static; width:32px; height:32px; box-shadow:none; border:none; background:transparent;">
|
| 41 |
+
<svg style="width:18px;height:18px;" 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>
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<button class="btn-new-chat" id="btnNewChat">
|
| 46 |
+
<svg style="width:16px; height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
| 47 |
+
New Workspace
|
| 48 |
+
</button>
|
| 49 |
+
|
| 50 |
+
<div class="sidebar-history" id="chatHistory"></div>
|
| 51 |
+
|
| 52 |
+
<div class="sidebar-footer">
|
| 53 |
+
<div class="user-avatar" id="uAv">G</div>
|
| 54 |
+
<div class="user-info-box">
|
| 55 |
+
<div class="user-name" id="uName">Guest Session</div>
|
| 56 |
+
<div class="user-sub" id="uSub">Queries: 0/10</div>
|
| 57 |
+
</div>
|
| 58 |
+
<button class="btn-login-register" id="btnLoginRegister">Access</button>
|
| 59 |
+
<button class="btn-power" id="btnDeleteAcc" style="display:none;" title="Delete Account">
|
| 60 |
+
<svg style="width:16px;height:16px;" 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>
|
| 61 |
+
</button>
|
| 62 |
+
<button class="btn-power" id="btnLogout" style="display:none;" title="Logout">
|
| 63 |
+
<svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
|
| 64 |
+
</button>
|
| 65 |
+
</div>
|
| 66 |
+
</aside>
|
| 67 |
+
|
| 68 |
+
<!-- Main Chat Area -->
|
| 69 |
+
<main class="main-area">
|
| 70 |
+
<div class="chat-container" id="chatContainer">
|
| 71 |
+
<div class="welcome-center" id="welcomeScreen">
|
| 72 |
+
<img id="welcomeLogoImg" alt="CODE VED">
|
| 73 |
+
<h1 id="welcomeTitle">Hello, Guest!</h1>
|
| 74 |
+
<p id="welcomeGreeting">How can I help you today?</p>
|
| 75 |
+
</div>
|
| 76 |
+
<div id="chatMessages"></div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<button id="scrollToBottomBtn" class="btn-scroll-bottom">
|
| 80 |
+
<svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
|
| 81 |
+
</button>
|
| 82 |
+
|
| 83 |
+
<!-- Input Dock -->
|
| 84 |
+
<div class="input-dock" id="inputDock">
|
| 85 |
+
<div class="input-dock-wrapper" id="dockWrapper">
|
| 86 |
+
<div class="loc-toast" id="locStatus"></div>
|
| 87 |
+
|
| 88 |
+
<div class="file-preview-bar" id="filePreviewBar">
|
| 89 |
+
<div class="file-preview-pill">
|
| 90 |
+
<img id="imgPreview" class="file-preview-icon" src="" style="display:none;">
|
| 91 |
+
<svg id="docPreview" style="display:none; width:14px; height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
|
| 92 |
+
<div class="file-preview-name" id="fileName">doc.pdf</div>
|
| 93 |
+
<button class="btn-remove-file" id="btnRemoveFile">
|
| 94 |
+
<svg style="width:12px;height:12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
| 95 |
+
</button>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<textarea class="chat-input" id="mainInput" placeholder="Ask CODE VED..." rows="1"></textarea>
|
| 100 |
+
|
| 101 |
+
<div class="input-row">
|
| 102 |
+
<div class="tools-left">
|
| 103 |
+
<div style="position: relative;">
|
| 104 |
+
<button class="tool-btn" id="btnAttach" title="Attach File">
|
| 105 |
+
<svg style="width:20px;height:20px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
|
| 106 |
+
</button>
|
| 107 |
+
|
| 108 |
+
<!-- Attach Dropdown -->
|
| 109 |
+
<div class="dropdown-menu" id="attachMenu">
|
| 110 |
+
<div class="dropdown-item" id="optUploadImg">
|
| 111 |
+
<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>
|
| 112 |
+
Upload Image
|
| 113 |
+
</div>
|
| 114 |
+
<div class="dropdown-item" id="optUploadDoc">
|
| 115 |
+
<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>
|
| 116 |
+
Upload Document
|
| 117 |
+
</div>
|
| 118 |
+
<div style="height:1px; background:var(--border-light); margin:4px 0;"></div>
|
| 119 |
+
<div class="dropdown-item" id="optEnv">
|
| 120 |
+
<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>
|
| 121 |
+
GPS & Weather
|
| 122 |
+
</div>
|
| 123 |
+
<div class="dropdown-item" id="optThink">
|
| 124 |
+
<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>
|
| 125 |
+
Thinking Mode
|
| 126 |
+
</div>
|
| 127 |
+
<div class="dropdown-item" id="optVoice">
|
| 128 |
+
<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>
|
| 129 |
+
Select Voice
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<!-- Voice Dropdown -->
|
| 134 |
+
<div class="dropdown-menu" id="voiceMenu" style="min-width: 220px; left: 45px; bottom: 40px; max-height: 300px; overflow-y: auto;">
|
| 135 |
+
<div class="dropdown-header">Premium Voices</div>
|
| 136 |
+
<div id="voiceOptionsList"></div>
|
| 137 |
+
<div style="height:1px; background:var(--border-light); margin:4px 0;"></div>
|
| 138 |
+
<div class="dropdown-header">Language</div>
|
| 139 |
+
<div id="languageOptionsList"></div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<!-- Thinking Dropdown -->
|
| 143 |
+
<div class="dropdown-menu" id="thinkMenu" style="min-width: 180px; left: 45px; bottom: 40px;">
|
| 144 |
+
<div class="dropdown-header">Reasoning Effort</div>
|
| 145 |
+
<div class="dropdown-item" data-effort="low">⚡ Low (Fast)</div>
|
| 146 |
+
<div class="dropdown-item" data-effort="medium">🧠 Medium (Balanced)</div>
|
| 147 |
+
<div class="dropdown-item" data-effort="high">🔍 High (Deep logic)</div>
|
| 148 |
+
<div style="height:1px; background:var(--border-light); margin:4px 0;"></div>
|
| 149 |
+
<div class="dropdown-item" id="optDisableThink" style="color:var(--brand-danger);">❌ Disable Thinking</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- Hidden File Inputs -->
|
| 153 |
+
<input type="file" id="imgUpload" class="hidden-input" accept="image/*">
|
| 154 |
+
<input type="file" id="docUpload" class="hidden-input" accept=".pdf,.txt,.docx,.html,.js,.py,.css,.cpp,.c,.json,.md">
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<button class="tool-btn" id="btnSearch" title="Web Search">
|
| 158 |
+
<svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
| 159 |
+
</button>
|
| 160 |
+
|
| 161 |
+
<button class="tool-btn btn-mic" id="btnStt" title="Voice Typing">
|
| 162 |
+
<svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
| 163 |
+
</button>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div class="tools-right">
|
| 167 |
+
<button class="btn-stop" id="btnStop">
|
| 168 |
+
<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"></rect></svg>
|
| 169 |
+
</button>
|
| 170 |
+
<button class="btn-send" id="btnSend">
|
| 171 |
+
<svg style="width:18px;height:18px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
|
| 172 |
+
</button>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</main>
|
| 178 |
+
|
| 179 |
+
<!-- Auth Modal -->
|
| 180 |
+
<div class="auth-overlay" id="authModal">
|
| 181 |
+
<div class="auth-modal">
|
| 182 |
+
<button class="auth-close" id="btnAuthClose">
|
| 183 |
+
<svg style="width:18px;height:18px;" 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>
|
| 184 |
+
</button>
|
| 185 |
+
<div class="auth-title">System Access</div>
|
| 186 |
+
<div class="auth-tabs">
|
| 187 |
+
<div class="auth-tab active" id="tabLogin" data-tab="login">Login</div>
|
| 188 |
+
<div class="auth-tab" id="tabRegister" data-tab="register">Register</div>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
<!-- Login Flow -->
|
| 192 |
+
<div id="flowLogin">
|
| 193 |
+
<div class="auth-phase active" id="loginPhase1">
|
| 194 |
+
<input type="email" class="auth-input" id="logEmail" placeholder="Email Address">
|
| 195 |
+
<button class="auth-btn" id="btnLogOtp">Continue</button>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="auth-phase" id="loginPhase2">
|
| 198 |
+
<input type="number" class="auth-input" id="logOtp" placeholder="Enter OTP">
|
| 199 |
+
<button class="auth-btn" id="btnLogVerify">Verify</button>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<!-- Register Flow -->
|
| 204 |
+
<div id="flowRegister" style="display:none;">
|
| 205 |
+
<div class="auth-phase active" id="regPhase1">
|
| 206 |
+
<input type="text" class="auth-input" id="regName" placeholder="Full Name">
|
| 207 |
+
<input type="email" class="auth-input" id="regEmail" placeholder="Email Address">
|
| 208 |
+
<button class="auth-btn" id="btnRegOtp">Create Account</button>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="auth-phase" id="regPhase2">
|
| 211 |
+
<input type="number" class="auth-input" id="regOtp" placeholder="Enter OTP">
|
| 212 |
+
<button class="auth-btn" id="btnRegVerify">Verify</button>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
<div class="auth-message" id="authMsg"></div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<!-- Custom Confirm Overlay -->
|
| 220 |
+
<div class="custom-confirm-overlay" id="customConfirmOverlay">
|
| 221 |
+
<div class="custom-confirm-box">
|
| 222 |
+
<div class="custom-confirm-msg" id="customConfirmMsg"></div>
|
| 223 |
+
<div class="custom-confirm-btns">
|
| 224 |
+
<button class="custom-confirm-btn cancel" id="customConfirmCancel">Cancel</button>
|
| 225 |
+
<button class="custom-confirm-btn confirm" id="customConfirmOk">Confirm</button>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<!-- External Libraries JS -->
|
| 231 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
| 232 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
|
| 233 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
| 234 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
| 235 |
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
| 236 |
+
|
| 237 |
+
<!-- MathJax Configuration -->
|
| 238 |
+
<script>
|
| 239 |
+
window.MathJax = {
|
| 240 |
+
tex: {
|
| 241 |
+
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
| 242 |
+
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
| 243 |
+
processEscapes: true,
|
| 244 |
+
packages: { '[+]': ['mhchem', 'physics', 'cancel', 'color', 'amsmath', 'amssymb', 'boldsymbol'] }
|
| 245 |
+
},
|
| 246 |
+
loader: {
|
| 247 |
+
load: ['[tex]/mhchem', '[tex]/physics', '[tex]/cancel', '[tex]/color', '[tex]/amsmath', '[tex]/amssymb', '[tex]/boldsymbol']
|
| 248 |
+
},
|
| 249 |
+
options: {
|
| 250 |
+
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
|
| 251 |
+
ignoreHtmlClass: 'tex2jax_ignore',
|
| 252 |
+
processHtmlClass: 'tex2jax_process'
|
| 253 |
+
},
|
| 254 |
+
startup: { typeset: false }
|
| 255 |
+
};
|
| 256 |
+
</script>
|
| 257 |
+
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
| 258 |
+
|
| 259 |
+
<!-- Custom Modular JS Files (Placeholders for next steps) -->
|
| 260 |
+
<!-- Markdown Renderer must be loaded before main script -->
|
| 261 |
+
<script type="module" src="js/markdownRenderer.js"></script>
|
| 262 |
+
<script type="module" src="js/script.js"></script>
|
| 263 |
+
|
| 264 |
+
</body>
|
| 265 |
+
</html>
|
js/markdownRenderer.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ================================================================================
|
| 2 |
+
// CODE VED - ADVANCED MARKDOWN & MATH RENDERER (markdownRenderer.js)
|
| 3 |
+
// Engineered by Divy Patel | Modular Architecture
|
| 4 |
+
//
|
| 5 |
+
// This module is the "Visual Engine" of the AI. It handles:
|
| 6 |
+
// 1. Markdown parsing (via marked.js)
|
| 7 |
+
// 2. Syntax Highlighting (via highlight.js)
|
| 8 |
+
// 3. Complex Math Formulas (via MathJax) with pre/post-processing to prevent breaking
|
| 9 |
+
// 4. Mermaid Diagrams rendering
|
| 10 |
+
// 5. AI "Thinking Process" collapsible UI
|
| 11 |
+
// 6. Code block copy buttons
|
| 12 |
+
// ================================================================================
|
| 13 |
+
|
| 14 |
+
(function() {
|
| 15 |
+
'use strict';
|
| 16 |
+
|
| 17 |
+
// ----------------------------------------------------------------------------
|
| 18 |
+
// 1. INITIALIZATION & CONFIGURATION
|
| 19 |
+
// ----------------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
// Initialize Mermaid (Diagrams)
|
| 22 |
+
function initMermaid() {
|
| 23 |
+
if (typeof mermaid !== 'undefined') {
|
| 24 |
+
mermaid.initialize({
|
| 25 |
+
startOnLoad: false,
|
| 26 |
+
theme: 'default',
|
| 27 |
+
securityLevel: 'loose',
|
| 28 |
+
fontFamily: 'Inter, sans-serif'
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Configure Marked.js (Markdown Parser)
|
| 34 |
+
function initMarked() {
|
| 35 |
+
if (typeof marked === 'undefined') return;
|
| 36 |
+
|
| 37 |
+
const renderer = new marked.Renderer();
|
| 38 |
+
|
| 39 |
+
// Custom Code Block Renderer (Handles Syntax Highlighting & Mermaid)
|
| 40 |
+
renderer.code = function(code, language, isEscaped) {
|
| 41 |
+
// Handle Mermaid Diagrams
|
| 42 |
+
if (language === 'mermaid') {
|
| 43 |
+
return `<pre class="mermaid-diagram"><code class="mermaid">${code}</code></pre>`;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Handle Syntax Highlighting
|
| 47 |
+
let highlightedCode = code;
|
| 48 |
+
const langClass = language ? `language-${language}` : '';
|
| 49 |
+
|
| 50 |
+
if (language && typeof hljs !== 'undefined' && hljs.getLanguage(language)) {
|
| 51 |
+
try {
|
| 52 |
+
highlightedCode = hljs.highlight(code, { language: language, ignoreIllegals: true }).value;
|
| 53 |
+
} catch (e) {
|
| 54 |
+
highlightedCode = hljs.highlightAuto(code).value;
|
| 55 |
+
}
|
| 56 |
+
} else if (typeof hljs !== 'undefined') {
|
| 57 |
+
highlightedCode = hljs.highlightAuto(code).value;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// SVG Icons for Copy Button
|
| 61 |
+
const copyIcon = `<svg 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>`;
|
| 62 |
+
|
| 63 |
+
// Return formatted code block with a copy button
|
| 64 |
+
return `<pre class="code-block-wrapper"><button class="code-copy-btn" title="Copy code">${copyIcon}<span>Copy</span></button><code class="hljs ${langClass}">${highlightedCode}</code></pre>`;
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
// Apply the custom renderer to marked
|
| 68 |
+
marked.use({
|
| 69 |
+
renderer: renderer,
|
| 70 |
+
gfm: true,
|
| 71 |
+
breaks: true,
|
| 72 |
+
pedantic: false,
|
| 73 |
+
mangle: false,
|
| 74 |
+
headerIds: false
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// ----------------------------------------------------------------------------
|
| 79 |
+
// 2. MATHJAX PRE-PROCESSING & POST-PROCESSING (The Secret Sauce)
|
| 80 |
+
// ----------------------------------------------------------------------------
|
| 81 |
+
/*
|
| 82 |
+
* WHY THIS IS NEEDED:
|
| 83 |
+
* Markdown parsers (like marked) often break LaTeX math syntax. For example,
|
| 84 |
+
* underscores (_) in math formulas get converted to italics (<em>), and
|
| 85 |
+
* asterisks (*) get converted to bold (<strong>).
|
| 86 |
+
* To prevent this, we temporarily hide math blocks using unique placeholders,
|
| 87 |
+
* parse the markdown, and then put the math blocks back!
|
| 88 |
+
*/
|
| 89 |
+
|
| 90 |
+
function preprocessMath(text) {
|
| 91 |
+
const mathBlocks = {};
|
| 92 |
+
let counter = 0;
|
| 93 |
+
|
| 94 |
+
function createPlaceholder(match) {
|
| 95 |
+
const id = `@@MATHBLOCK_${counter++}@@`;
|
| 96 |
+
mathBlocks[id] = match;
|
| 97 |
+
return id;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 1. Display Math: $$ ... $$
|
| 101 |
+
text = text.replace(/\$\$([\s\S]+?)\$\$/g, createPlaceholder);
|
| 102 |
+
|
| 103 |
+
// 2. Display Math: \[ ... \]
|
| 104 |
+
text = text.replace(/\\\[([\s\S]+?)\\\]/g, createPlaceholder);
|
| 105 |
+
|
| 106 |
+
// 3. Inline Math: $ ... $ (Ensuring it doesn't match currency or empty spaces)
|
| 107 |
+
text = text.replace(/(?<!\$)\$(?!\$)((?:[^$\\]|\\.)+?)\$(?!\$)/g, createPlaceholder);
|
| 108 |
+
|
| 109 |
+
// 4. Inline Math: \( ... \)
|
| 110 |
+
text = text.replace(/\\\(([\s\S]+?)\\\)/g, createPlaceholder);
|
| 111 |
+
|
| 112 |
+
return { text, mathBlocks };
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
function postprocessMath(html, mathBlocks) {
|
| 116 |
+
// Replace all placeholders back with the original LaTeX strings
|
| 117 |
+
for (const [id, mathStr] of Object.entries(mathBlocks)) {
|
| 118 |
+
html = html.split(id).join(mathStr);
|
| 119 |
+
}
|
| 120 |
+
return html;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ----------------------------------------------------------------------------
|
| 124 |
+
// 3. THINKING PROCESS EXTRACTION (AI Reasoning UI)
|
| 125 |
+
// ----------------------------------------------------------------------------
|
| 126 |
+
|
| 127 |
+
function processThinkingBlocks(text) {
|
| 128 |
+
// Normalize different AI thinking tags (Qwen, etc.)
|
| 129 |
+
let normalizedText = text
|
| 130 |
+
.replace(/<\|channel\|>thought\s*<\|channel\|>/gi, "<think>\n")
|
| 131 |
+
.replace(/<\|channel\|>answer\s*<\|channel\|>/gi, "\n</think>\n")
|
| 132 |
+
.replace(/<\|im_start\|>thought/gi, "<think>\n")
|
| 133 |
+
.replace(/<\|im_end\|>/gi, "\n</think>\n");
|
| 134 |
+
|
| 135 |
+
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/i;
|
| 136 |
+
const thinkMatch = normalizedText.match(thinkRegex);
|
| 137 |
+
|
| 138 |
+
let thinkHtml = '';
|
| 139 |
+
let mainText = normalizedText;
|
| 140 |
+
|
| 141 |
+
if (thinkMatch) {
|
| 142 |
+
const thinkContent = thinkMatch[1].trim();
|
| 143 |
+
// Parse the thinking content as markdown too, for better readability
|
| 144 |
+
const parsedThinkContent = typeof marked !== 'undefined' ? marked.parse(thinkContent) : thinkContent;
|
| 145 |
+
|
| 146 |
+
thinkHtml = `
|
| 147 |
+
<details class="qwen-think-box" open>
|
| 148 |
+
<summary>
|
| 149 |
+
<svg class="arrow" style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 150 |
+
<polyline points="9 18 15 12 9 6"></polyline>
|
| 151 |
+
</svg>
|
| 152 |
+
Thinking Process
|
| 153 |
+
</summary>
|
| 154 |
+
<div class="qwen-think-content">${parsedThinkContent}</div>
|
| 155 |
+
</details>`;
|
| 156 |
+
|
| 157 |
+
// Remove the think block from the main text
|
| 158 |
+
mainText = normalizedText.replace(thinkRegex, '').trim();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return { thinkHtml, mainText };
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// ----------------------------------------------------------------------------
|
| 165 |
+
// 4. POST-RENDERING TRIGGERS (MathJax & Mermaid)
|
| 166 |
+
// ----------------------------------------------------------------------------
|
| 167 |
+
|
| 168 |
+
async function triggerMathJax(container) {
|
| 169 |
+
if (window.MathJax && MathJax.typesetPromise) {
|
| 170 |
+
try {
|
| 171 |
+
// Clear previous math rendering in this container to prevent errors
|
| 172 |
+
MathJax.typesetClear([container]);
|
| 173 |
+
// Render new math
|
| 174 |
+
await MathJax.typesetPromise([container]);
|
| 175 |
+
} catch (err) {
|
| 176 |
+
console.warn('MathJax Rendering Warning:', err);
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
async function triggerMermaid(container) {
|
| 182 |
+
if (typeof mermaid === 'undefined') return;
|
| 183 |
+
|
| 184 |
+
const diagrams = container.querySelectorAll('.mermaid');
|
| 185 |
+
if (diagrams.length === 0) return;
|
| 186 |
+
|
| 187 |
+
try {
|
| 188 |
+
// mermaid.run is the modern way to render specific nodes
|
| 189 |
+
await mermaid.run({ nodes: diagrams });
|
| 190 |
+
} catch (err) {
|
| 191 |
+
console.warn('Mermaid Rendering Warning:', err);
|
| 192 |
+
// If mermaid fails, it might leave broken SVGs. We clean them up.
|
| 193 |
+
diagrams.forEach(d => {
|
| 194 |
+
if (!d.querySelector('svg')) {
|
| 195 |
+
d.innerHTML = `<span style="color:var(--brand-danger);font-size:12px;">⚠️ Invalid Mermaid Syntax</span>`;
|
| 196 |
+
}
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// ----------------------------------------------------------------------------
|
| 202 |
+
// 5. MAIN RENDERING ENGINE (Exposed to script.js)
|
| 203 |
+
// ----------------------------------------------------------------------------
|
| 204 |
+
|
| 205 |
+
/**
|
| 206 |
+
* The core function called by script.js to render AI responses.
|
| 207 |
+
* @param {string} fullText - The raw text from the AI.
|
| 208 |
+
* @param {boolean} isProcessing - True if the AI is still streaming.
|
| 209 |
+
* @param {HTMLElement} container - The DOM element to inject the HTML into.
|
| 210 |
+
*/
|
| 211 |
+
async function parseAndRender(fullText, isProcessing, container) {
|
| 212 |
+
if (!container) return;
|
| 213 |
+
|
| 214 |
+
// Step 1: Extract and format "Thinking" blocks
|
| 215 |
+
const { thinkHtml, mainText } = processThinkingBlocks(fullText);
|
| 216 |
+
|
| 217 |
+
let finalHtml = thinkHtml;
|
| 218 |
+
|
| 219 |
+
// Step 2: Process the main text
|
| 220 |
+
if (mainText) {
|
| 221 |
+
// 2a. Hide Math formulas so Markdown parser doesn't break them
|
| 222 |
+
const { text: safeText, mathBlocks } = preprocessMath(mainText);
|
| 223 |
+
|
| 224 |
+
// 2b. Parse Markdown to HTML
|
| 225 |
+
let parsedHtml = '';
|
| 226 |
+
if (typeof marked !== 'undefined') {
|
| 227 |
+
parsedHtml = marked.parse(safeText);
|
| 228 |
+
} else {
|
| 229 |
+
parsedHtml = safeText.replace(/\n/g, '<br>'); // Fallback
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// 2c. Put Math formulas back
|
| 233 |
+
parsedHtml = postprocessMath(parsedHtml, mathBlocks);
|
| 234 |
+
|
| 235 |
+
// 2d. Wrap in a class for MathJax targeting
|
| 236 |
+
finalHtml += `<div class="tex2jax_process">${parsedHtml}</div>`;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// Step 3: Add blinking cursor if AI is still generating
|
| 240 |
+
if (isProcessing) {
|
| 241 |
+
finalHtml += `<span class="blinking-cursor"></span>`;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Step 4: Inject into DOM
|
| 245 |
+
container.innerHTML = finalHtml;
|
| 246 |
+
|
| 247 |
+
// Step 5: Trigger external renderers (MathJax & Mermaid)
|
| 248 |
+
// We only trigger them fully when processing is done to save performance,
|
| 249 |
+
// but we do a lightweight pass during streaming.
|
| 250 |
+
if (!isProcessing) {
|
| 251 |
+
await triggerMathJax(container);
|
| 252 |
+
await triggerMermaid(container);
|
| 253 |
+
} else {
|
| 254 |
+
// Lightweight MathJax trigger during streaming (optional, prevents lag)
|
| 255 |
+
// triggerMathJax(container);
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// ----------------------------------------------------------------------------
|
| 260 |
+
// 6. INITIALIZATION & EXPORT
|
| 261 |
+
// ----------------------------------------------------------------------------
|
| 262 |
+
|
| 263 |
+
function init() {
|
| 264 |
+
initMermaid();
|
| 265 |
+
initMarked();
|
| 266 |
+
console.log('[MarkdownRenderer] Engine initialized successfully.');
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// Expose the API to the global window object so script.js can use it
|
| 270 |
+
window.MarkdownRenderer = {
|
| 271 |
+
parseAndRender: parseAndRender,
|
| 272 |
+
init: init
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
// Auto-initialize when this script loads
|
| 276 |
+
init();
|
| 277 |
+
|
| 278 |
+
})();
|
js/script.js
ADDED
|
@@ -0,0 +1,1221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ================================================================================
|
| 2 |
+
// CODE VED - MAIN APPLICATION LOGIC (script.js)
|
| 3 |
+
// Engineered by Divy Patel | Modular Architecture
|
| 4 |
+
// ================================================================================
|
| 5 |
+
|
| 6 |
+
(function() {
|
| 7 |
+
'use strict';
|
| 8 |
+
|
| 9 |
+
// ----------------------------------------------------------------------------
|
| 10 |
+
// 1. CONFIGURATION & GLOBAL STATE
|
| 11 |
+
// ----------------------------------------------------------------------------
|
| 12 |
+
const Config = {
|
| 13 |
+
GAS_URL: "https://script.google.com/macros/s/AKfycbzNO3inVc33ImhfLyde-JjjK9ZlPckLBksqCnCzelfhcklX6mp8KW8vfPTW4oWJTCcN/exec",
|
| 14 |
+
API_ENDPOINT: "/api/chat",
|
| 15 |
+
LOGO: "data:image/webp;base64,UklGRngQAABXRUJQVlA4IGwQAACQWgCdASpAAUABPlEokUYjoqGhJDJ4qHAKCWNu4DBns+F/EVjrSh+zk+4R9RfyzWR4//Bh3XP//hlAO+I89uyf2z8I9WxVXm98m/8T+9fkr73P8H7EPuG9wL+Ffyb/f/3TrP/13/ieoD+n/4b/p/3H3u/R9/Xv3A9wD+m/77rFvQA/cD0yf3F+D7+sf7P9uPaI/93sAf/X1AOJU0Mfc5xFtN+ymWkEl5KCraa75Bv271AOiB+7gqyT4a/7bd5MK0Vz2BF/HPf+0eHenyM5HgG6oNwZy7j/QkukOiqCLuzh6j/SFK9Fc+VghhMyuOHyz1H+RRftuL9btmL1G3eahLnulodE91fhr0j6uoD8ohpPSBe/U3fbRa0pHPLGfPuFeI3tIa1IcNFUKvWJGVQHGAl6e/o4jfMGyvTigndfWOE4tSCSmvQU9C8h3Je6ce6OWwT4q/K5sCPY5uC38qmFoH1xWQI8KPhRXhTH+mcJp2iWA81EwNSXWn+6mvEITXX0asRDAZ5Pk+GOIiO+5qAF9FkIYjuZfJtao1wnpsY++BYFcXdnBjcjW12y3mQM4MzBtqMaEsHiLgq0fo0J8+gr3I5Y79RAzarMFQ9fhtB5YiLwLj5OFjVkbQFJaZdKr6SByJ72sAnGFZiH2AlO1x4fbQuEn2zPs0ZCS0i7RIvuP067pTJ87mWOwzqBr1D0zEkwys6iAxgSlxVZazf9HR2EiRatMXGabM4rNri9nXnlsNv5W92HDieYDMqSt1/LboHQQWD80fzVC9htPA0CKP9A9Tq4kk5x7en1zRRIjj+dg2GZja0DXrXaz7XaFbEMiCZhXI+1vXKVyBGCoNl5xuIyGM3NV6FUIUAtQU0SEOV4F1voaL+V/JI5FWpbNDHBYvE+GxQqVahfEK0Vz5IqMxMMQRg6IgmlHJca3HhZNMJiYa/V+WmQa3a58q/j9XosxdlAY1hvY7zUJdIcNgEMLJnEAP7w81z6enC1GoY3//59L/yL/OReQP5mMOvEIYFvavtgWP0ir6ET2EaDgeFlf9V//4eQowVLAW9qu0MTbVGUkVoF7b8OOa0RPolLBqIksyByCZyqcgi+qKKV/KbIo8eOgf/GU/FcohlS3ck9Huos7o59SF7MUgcciPFnPr996An4PP7Qu7hAHtCZ2ncxk6vUQZ7UoMEOTxxzA/Mn+qbXAzEywYP8eDfEUFt8FwCItXjfUefJrArUfQm9M24EzGyzcQoUpU4JVBxHQdemyP0jVxuHd5dv4yBtaDK+tL5Ki6pLn7x2Uh7zwmxKR2HfnaZSGe0LZrl8VXuJ2fBFCnP3GBMi2WURM2E9zatjDQmexuUVgMukaukjCwGAgTiX77U0uGQ2nTlQMnSNYOucrsKJm2V/aA8nZGI9DSbnwGjn2XYoQTJy9BuWIW3x3APbgGM/qF6SmI77SrzeRzohPKBUf+5M40lOebd54Bp7r7WN1gO+a0z09IGVk7FgmdItK1Hl8dkxCvmeDZoWjq1amu/jvvbhhHhcHBXsG3ZqzgVWpdpQH+tmfv3fU6otRMr+PwpC6taPs0fNslHdeNJbY2lhKuJCnjx0ayV+8ga7HTGmzddVZEs/aMX+e9zfUmhk3RK/6ZtRqdXSsYwHf1/HQuUNlsT1T00AiJIqTj3uU7sFuvHsQWsE/MEJrPc+0CHdAXS0tuuDbuyQvN5ZxQcYly/l2iAnkkELaNVsk3YeUnVz6qSSaHz6PbhjUAITOPDa4QHeTp9GceAOV+MyYr8zkvol9EYXcvJLqbf6vi+TrpPXxBzxZN/p7q0tuevG1GO7VvB7iy1PpM9fmgr3e9OjHumy+LCRLegd80O3eIqggzlu/BexD/72jR/+vXtD2R//pZauVznunW2V4YoDuxR0P//0AKJSN/6af6pQDLqs2+39zwoYvB+HyQtgs0Uetrx9WvVhkpH9n9kd5vUg91VefJuiWHjQV7JGeYGdag0aDLo4THXNjphUx8g1KzXUUef05sKbvJ3YMgV3GUD/mDw9cdX8yY8gbTEUHGTdfGcNKyygEP2e4e9nz6IgbAcfSWD5IUxC7cpJVe3goPla/kGxWXN39c8InXYfMOH2MNHbuqTDzV9qdYZlonyhmcW9O5zHMuevL/zgMxAkPo6hnbPYVjPbkTIWAbYQTTT0hM22ZIz3Io/1z4XOE46Et7jX2EJlI4qqGIQGDkIWN3mkp9z9JMA8P9OPLEfeiZt5hSJzlM4WxAl5HKpYLYw8lYbX9wqKRGS2tIm6uDHBMwLgM98DCuLmcai3dh17Iylln4Mr6FIj7dYmIhce1G+0d9Yys+yFU78CuHFcrnL15mQxv3pnBAwyFl47e20Xf4rh8VKm/HSr3FpDfnGoPpGW5YHxUz5XhjEUnfKnT4Sy6uicm/6z/Zqxt5FfVpwDdlO3oO69ujQL8ziKVeC9w1Q75VZY54ScNi5g5jHWoLoJvjSUtY5gMiiMgmTmQZp0AZIVb5kHidFIAKkyggL2Qtu1unWX3IOdYg/wvxh9gGdoF+9ZdJ9E/hP0G9BZqZULm2Jp3BVdRSAepFq54T1cSnDQuF3moJtJJkPpMOpj9pR/R9/h+TEIvxCc6UsBx6Gmx2UAGGKuIHSXfQrCIJoR7e10TVssNHbyZqiln99iCU8pW9jLq96QM4YAEd9PhxviLn8/aKIbnvNl/YTLYPy4YfpwHOt8GmD0D7WUhCDUvOuhB/9QnRJTkiHaTXXWjNNbGuCyKtV0ySOMIp2dmfPgIRrj1+ZaVrw7yKeQsryqveSmCz3O1kbgdbmg3BkPjAo/Lch5r72Xd5uxEWYhdGbP2e7JvpmN0JyHKTiLsSgxgAcc1Jt8cya0lXiryZ9lYCH5MIbLGaiAp9lAqFm85foc9fD5zf+C7cxZE9DdZdCH8q/C0G8brSyAhzLaUf1yqi44/87jvYLrIunCfitc3/e8GqvQwy5F0BJ3hTsAORIhp2iz2HHCN9yTohT3yeC9e56UcL7ug3SCCNWNunU5S3OJkhr2GAw0kK4LuJxDZlqxxgtGrLo3zwDeYss0sDA87VzeJo/+573/thd/o7FrAdu10Po4iCIIaIVqrwV05JWb1gIGU0V+qxhGOuQQ49a+n/lUEAJYywT49wED7l4LPlkGCMCmI1SmhSCSQlcWc4x8btbcVkt19n6ZM4TEjEz0E3W32rqQ/tWRYirCIBzcLsDK08o9ZIjlgdd7rIK1QdvexVNpY4eqOVGH3MhNkmeaDypSI7HIkP7GlvclA9pYtbBD0rha7AEagY7woaVOz3RYTCxG9kUnfCqrWxRMtR/kEBJ6I0FeIWyGin5ofw77CYwF46TTDyi2Ls3m/dWH4SA/eiZIihHaqqzgebYW+I37GIWUHBGSxVLmBPQZ5/0V+S8Nx+p/CdZL0LH76Cp0E7R3ApBwTRZfiC5Luo3i0dDiFsX+xhhAjBfjk0BW1MwZbbpXJkHae7/Gu1JP2R78J9qfCr+LTWGXa7XLWq1DVwY5JaJEWB0ThsyBDFmaoCoKRBRUys1LKLOzzFVEQqO+B6VY7K+EQ+xFSd+Sw1T6IG6yTmCTws1DnK8BKSGR5zLi11BlKdDS1VR7C2fEq7U28r4kJ5DC4kMg79jbJpCbuCF7Ubq9khHkYFDAVBeNPm7QMGHjE44+jQOCKrhC5HSN2B8FpjJwqNNgxqVjQgU7OeE8pttEaKWbqSuBqNkOsiDPWHJP1A1tySRStKPiO1w6dgZrYn1mNZlaznCfFwk18FtRY6ETwNB/Q9QacWkEYHDVfWMzti7CfQNuU5dyXNZdFnbg0sJro5xQFEGdSLXK5xblEJh0+FkApJYNhRGf1/no37TfeR+HWQizVi1n+Q6S/D/It21cWg14FQvsLua+KGhpL8jo3oK1giUelI6bSFfXm6ZBSFuK6V0wo8paeKoS6t/aQaFwPrcdWf5/q5H0K1WHeRVrsZ7ggEPFNPA5MwR+IeBy6L370NIXzZymUAXVgFUdW7Z134eKnNXmhx1T8G5UYxm6jdO+ubZ2+2ycB/DyviUCE/EsFJbae97QBthuykVKPqTD0htgra+FIAvpNpN3qW9vRNxKf/tWfuYa6gUUdQHcgwce7H/KTmQhxQ44vYHn9Aqi+ep+lY1qlXFBldCqwu8jO+fsCBi9Ng4+VkMQcvzjQL4PespVyEDdMXZr5Jd/20Ie1sbJnGgSzmlvBM/bbG4m9oMon+TP3IJ4l4Z24G2qOY0bOXtLY+v/reNMlxdSaVLx78dj8okQBpfRo+3QG6AX+hSnRo8TGwffFp3yS2N6sjp/SugPpwFnl++cQx6jklR9NTUbRnxFyi4er5IPMvIAAZjz6FC8kUX3AaG1UEBJpKb5MEitru8iCMU/XAuZ/0qTnfWeJkfyyyIzxxTsvMS963gMMOa37rX4o6fEX+KYxnu1Jfn07rw/XSl5MGO+ccoBBj8f6mC+3NH3LNh3ozauMep/2tkfsIV3j3n+9MRvQAdRyih0MAjQXIplXXkpo9ikZnvdB9NzL/r893A2QS1Y1xUn4wTWb0b2iyJWAfBcqb6FPfLTBtyyoSHwgG9UtvJT+WyaLB03BEzlZrCXTNmJi9d3CFAhUE8VsBiT8JdfNcmcBs8+mXe38OCyoUXAI5oq5a+N9+//tjPFn4MCUn4VE5+z8kSoAPGcBkE4VUr8WRe8f+UcKnxfSUq4j1r1lrrwcTEbsj3IG10FcIrmtNopSgSs4zjD8hIj2mLxfc/d1sW0DVIfYnhosjc+sbSMATyduc2waAOxWMVaQc/koilAYQ0e2TS6fV9zIDHLMO1Oc/8E3d4BUwW0J1+e4KvdBypuV+FVrhcxMZJmc3lBviJ6iEzVlvn8VwlCvp8E+1TFZQJGrrotwMuqTrBww+Aa2QjDUHTuEdY6LTX5h8ykwGeajr9Nr8aHz/8fbv4b/o01guzBt6/u5vyUAq1s5fo4Yt+1Fy0ofiumKaWW+bhYI60TQbrzp4PI6CX5br5m1rYYzal6IQ55o5od0+EredtI9OPfV62/M4ShJlgTQODZfQz0dqf1OaTHw2fLpoFoHvc67Ny9KDmfROMFlu3WuzFMQA3ZTNaXpKYaoIpxTCBd+YjOu+ne6woMLN97Hr5jqZkJPdJllJAk0sSGlbHUUd4KElUnm7jwzGKN/rFGpvWbrYgqGZHtF7ot6WxvT/79mMDQMDiSmFeLBee6MD5+8TV8HD9q0uZEv87CYFuBL2v4rAsi5sCPjJxWovfAoapowXfTPheMdmFhq+rD5a0f2eLbXrX9O22VsakFzndaq9TxRKST2f783nDIbvWolpsMRc4UYMs3FXm/CCdy+DzisGFXgdOq2Frzi3AxT34U5PdQ48bhiKXURD/aU7KS5Ko0hTSzkvozEPjU42c4s3xIIKt1yUQ7ZRwgk8iD7T4H/emJNNsA0NUIge6Sy5HD6z1CS9BJTP1mgXWTu8MNE/LT/b3I9TDKSfVsYQ9VR52dPBWRricl2LpNeTynaY6j3IVVx8nG3V3Rt8C2U4TCELm5ImVO22NAwBw6r7PxecdaJYUO7V1q+vov9bPRrJtnKaCHAfFnyPQjaXASmqsowtAhyj1YdMYCx9kyB6cZlRAA"
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const State = {
|
| 19 |
+
user: localStorage.getItem('codeved_user') || null,
|
| 20 |
+
name: localStorage.getItem('codeved_name') || null,
|
| 21 |
+
guestCount: (() => {
|
| 22 |
+
const v = parseInt(localStorage.getItem('codeved_guest') || '0', 10);
|
| 23 |
+
return isNaN(v) ? 0 : v;
|
| 24 |
+
})(),
|
| 25 |
+
attachment: null,
|
| 26 |
+
isProcessing: false,
|
| 27 |
+
history: [],
|
| 28 |
+
currentThreadId: null,
|
| 29 |
+
currentTitle: null,
|
| 30 |
+
abortController: null,
|
| 31 |
+
location: null,
|
| 32 |
+
weatherContext: null,
|
| 33 |
+
thinkingMode: false,
|
| 34 |
+
thinkingEffort: "medium",
|
| 35 |
+
searchEnabled: false,
|
| 36 |
+
lastUserMessage: null
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
if (typeof pdfjsLib !== 'undefined') {
|
| 40 |
+
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// ----------------------------------------------------------------------------
|
| 44 |
+
// 2. UTILITIES & CUSTOM CONFIRM
|
| 45 |
+
// ----------------------------------------------------------------------------
|
| 46 |
+
function customConfirm(message) {
|
| 47 |
+
return new Promise((resolve) => {
|
| 48 |
+
const overlay = document.getElementById('customConfirmOverlay');
|
| 49 |
+
const msgEl = document.getElementById('customConfirmMsg');
|
| 50 |
+
const cancelBtn = document.getElementById('customConfirmCancel');
|
| 51 |
+
const okBtn = document.getElementById('customConfirmOk');
|
| 52 |
+
|
| 53 |
+
msgEl.textContent = message;
|
| 54 |
+
overlay.style.display = 'flex';
|
| 55 |
+
|
| 56 |
+
function cleanup() {
|
| 57 |
+
overlay.style.display = 'none';
|
| 58 |
+
cancelBtn.removeEventListener('click', onCancel);
|
| 59 |
+
okBtn.removeEventListener('click', onOk);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function onCancel() { cleanup(); resolve(false); }
|
| 63 |
+
function onOk() { cleanup(); resolve(true); }
|
| 64 |
+
|
| 65 |
+
cancelBtn.addEventListener('click', onCancel);
|
| 66 |
+
okBtn.addEventListener('click', onOk);
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// ----------------------------------------------------------------------------
|
| 71 |
+
// 3. UI MANAGER
|
| 72 |
+
// ----------------------------------------------------------------------------
|
| 73 |
+
const UI = {
|
| 74 |
+
autoScroll: true,
|
| 75 |
+
|
| 76 |
+
toggleSidebar() {
|
| 77 |
+
const sb = document.getElementById('sidebar');
|
| 78 |
+
const overlay = document.getElementById('mobileOverlay');
|
| 79 |
+
sb.classList.toggle('collapsed');
|
| 80 |
+
overlay.classList.toggle('active', !sb.classList.contains('collapsed') && window.innerWidth <= 900);
|
| 81 |
+
},
|
| 82 |
+
|
| 83 |
+
toggleAttachMenu() {
|
| 84 |
+
document.getElementById('attachMenu').classList.toggle('active');
|
| 85 |
+
document.getElementById('thinkMenu').classList.remove('active');
|
| 86 |
+
document.getElementById('voiceMenu').classList.remove('active');
|
| 87 |
+
},
|
| 88 |
+
|
| 89 |
+
autoGrow(el) {
|
| 90 |
+
el.style.height = 'auto';
|
| 91 |
+
el.style.height = Math.min(el.scrollHeight, 180) + 'px';
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
scrollToBottom(force = false) {
|
| 95 |
+
const c = document.getElementById('chatContainer');
|
| 96 |
+
if (force || this.autoScroll) {
|
| 97 |
+
c.scrollTop = c.scrollHeight;
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
escape(s) {
|
| 102 |
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 103 |
+
},
|
| 104 |
+
|
| 105 |
+
showAuthMsg(msg, isError = true) {
|
| 106 |
+
const el = document.getElementById('authMsg');
|
| 107 |
+
el.innerText = msg;
|
| 108 |
+
el.style.color = isError ? 'var(--brand-danger)' : 'var(--brand-success)';
|
| 109 |
+
setTimeout(() => el.innerText = '', 4000);
|
| 110 |
+
},
|
| 111 |
+
|
| 112 |
+
updateUserInfo(name, email) {
|
| 113 |
+
if (name) {
|
| 114 |
+
State.name = name;
|
| 115 |
+
localStorage.setItem('codeved_name', name);
|
| 116 |
+
}
|
| 117 |
+
const dispName = State.name || email.split('@')[0];
|
| 118 |
+
document.getElementById('uAv').innerText = dispName.charAt(0).toUpperCase();
|
| 119 |
+
document.getElementById('uName').innerText = dispName;
|
| 120 |
+
document.getElementById('uSub').innerText = email;
|
| 121 |
+
document.getElementById('btnLogout').style.display = 'flex';
|
| 122 |
+
document.getElementById('btnDeleteAcc').style.display = 'flex';
|
| 123 |
+
document.getElementById('btnLoginRegister').style.display = 'none';
|
| 124 |
+
document.getElementById('welcomeTitle').innerHTML = `Hello, ${dispName}!<br>How can I help you today?`;
|
| 125 |
+
},
|
| 126 |
+
|
| 127 |
+
updateWelcomeScreen() {
|
| 128 |
+
document.getElementById('welcomeScreen').style.display = State.history.length === 0 ? 'flex' : 'none';
|
| 129 |
+
},
|
| 130 |
+
|
| 131 |
+
initListeners() {
|
| 132 |
+
// Sidebar & Overlays
|
| 133 |
+
document.getElementById('btnMenuOpen').addEventListener('click', () => this.toggleSidebar());
|
| 134 |
+
document.getElementById('btnCloseSidebar').addEventListener('click', () => this.toggleSidebar());
|
| 135 |
+
document.getElementById('mobileOverlay').addEventListener('click', () => this.toggleSidebar());
|
| 136 |
+
|
| 137 |
+
// Scroll to bottom
|
| 138 |
+
document.getElementById('scrollToBottomBtn').addEventListener('click', () => {
|
| 139 |
+
this.scrollToBottom(true);
|
| 140 |
+
this.autoScroll = true;
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
// Input Dock Expanding Logic
|
| 144 |
+
const inp = document.getElementById('mainInput');
|
| 145 |
+
const dockWrapper = document.getElementById('dockWrapper');
|
| 146 |
+
|
| 147 |
+
inp.addEventListener('focus', () => dockWrapper.classList.add('expanded'));
|
| 148 |
+
inp.addEventListener('blur', () => {
|
| 149 |
+
if (inp.value.trim() === '') dockWrapper.classList.remove('expanded');
|
| 150 |
+
});
|
| 151 |
+
inp.addEventListener('input', () => {
|
| 152 |
+
this.autoGrow(inp);
|
| 153 |
+
if (inp.value.trim() !== '') dockWrapper.classList.add('expanded');
|
| 154 |
+
});
|
| 155 |
+
inp.addEventListener('keydown', (e) => {
|
| 156 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 157 |
+
e.preventDefault();
|
| 158 |
+
Chat.handleSend();
|
| 159 |
+
}
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
// Chat Container Scroll Listener
|
| 163 |
+
const chatContainerEl = document.getElementById('chatContainer');
|
| 164 |
+
const scrollBtnEl = document.getElementById('scrollToBottomBtn');
|
| 165 |
+
|
| 166 |
+
chatContainerEl.addEventListener('scroll', () => {
|
| 167 |
+
const scrollBottom = chatContainerEl.scrollHeight - chatContainerEl.scrollTop - chatContainerEl.clientHeight;
|
| 168 |
+
this.autoScroll = scrollBottom <= 50;
|
| 169 |
+
scrollBtnEl.classList.toggle('visible', !this.autoScroll);
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
// Close dropdowns when clicking outside
|
| 173 |
+
document.addEventListener('click', (e) => {
|
| 174 |
+
if (!e.target.closest('.tools-left')) {
|
| 175 |
+
document.getElementById('attachMenu').classList.remove('active');
|
| 176 |
+
document.getElementById('thinkMenu').classList.remove('active');
|
| 177 |
+
document.getElementById('voiceMenu').classList.remove('active');
|
| 178 |
+
}
|
| 179 |
+
});
|
| 180 |
+
}
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
// ----------------------------------------------------------------------------
|
| 184 |
+
// 4. AUTH MANAGER
|
| 185 |
+
// ----------------------------------------------------------------------------
|
| 186 |
+
const Auth = {
|
| 187 |
+
init() {
|
| 188 |
+
if (State.user) {
|
| 189 |
+
UI.updateUserInfo(State.name, State.user);
|
| 190 |
+
HistoryManager.syncAllChats();
|
| 191 |
+
} else {
|
| 192 |
+
document.getElementById('uAv').innerText = "G";
|
| 193 |
+
document.getElementById('uName').innerText = "Guest Mode";
|
| 194 |
+
document.getElementById('uSub').innerText = `Queries: ${State.guestCount}/10`;
|
| 195 |
+
document.getElementById('btnLogout').style.display = 'none';
|
| 196 |
+
document.getElementById('btnDeleteAcc').style.display = 'none';
|
| 197 |
+
document.getElementById('btnLoginRegister').style.display = 'inline-flex';
|
| 198 |
+
document.getElementById('welcomeTitle').innerHTML = `Hello, Guest!<br>How can I help you today?`;
|
| 199 |
+
}
|
| 200 |
+
},
|
| 201 |
+
|
| 202 |
+
async handleLogout() {
|
| 203 |
+
const ok = await customConfirm("Are you sure you want to logout?");
|
| 204 |
+
if (ok) {
|
| 205 |
+
localStorage.removeItem('codeved_user');
|
| 206 |
+
localStorage.removeItem('codeved_name');
|
| 207 |
+
location.reload();
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
|
| 211 |
+
async handleDeleteAccount() {
|
| 212 |
+
const ok = await customConfirm("Are you sure you want to permanently delete your account and all data?");
|
| 213 |
+
if (ok) {
|
| 214 |
+
try {
|
| 215 |
+
await fetch(Config.GAS_URL, {
|
| 216 |
+
method: 'POST',
|
| 217 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 218 |
+
body: JSON.stringify({ action: "delete_account", email: State.user })
|
| 219 |
+
});
|
| 220 |
+
} catch(e) { console.error("Account delete error:", e); }
|
| 221 |
+
finally {
|
| 222 |
+
localStorage.removeItem('codeved_user');
|
| 223 |
+
localStorage.removeItem('codeved_name');
|
| 224 |
+
location.reload();
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
|
| 229 |
+
openModal() { document.getElementById('authModal').style.display = 'flex'; },
|
| 230 |
+
closeModal() { document.getElementById('authModal').style.display = 'none'; },
|
| 231 |
+
|
| 232 |
+
switchTab(tab) {
|
| 233 |
+
document.getElementById('tabLogin').classList.remove('active');
|
| 234 |
+
document.getElementById('tabRegister').classList.remove('active');
|
| 235 |
+
document.getElementById('flowLogin').style.display = 'none';
|
| 236 |
+
document.getElementById('flowRegister').style.display = 'none';
|
| 237 |
+
|
| 238 |
+
if (tab === 'login') {
|
| 239 |
+
document.getElementById('tabLogin').classList.add('active');
|
| 240 |
+
document.getElementById('flowLogin').style.display = 'block';
|
| 241 |
+
} else {
|
| 242 |
+
document.getElementById('tabRegister').classList.add('active');
|
| 243 |
+
document.getElementById('flowRegister').style.display = 'block';
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
|
| 247 |
+
switchPhase(from, to) {
|
| 248 |
+
document.getElementById(from).classList.remove('active');
|
| 249 |
+
document.getElementById(to).classList.add('active');
|
| 250 |
+
},
|
| 251 |
+
|
| 252 |
+
async process(action) {
|
| 253 |
+
let payload = { action: action };
|
| 254 |
+
let btnId = '';
|
| 255 |
+
|
| 256 |
+
if (action === 'register_send_otp') {
|
| 257 |
+
payload.name = document.getElementById('regName').value.trim();
|
| 258 |
+
payload.email = document.getElementById('regEmail').value.trim();
|
| 259 |
+
payload.phone = "0000000000";
|
| 260 |
+
payload.organization = "CODE VED";
|
| 261 |
+
if (!payload.name || !payload.email) return UI.showAuthMsg("Details missing.");
|
| 262 |
+
btnId = 'btnRegOtp';
|
| 263 |
+
} else if (action === 'login_send_otp') {
|
| 264 |
+
payload.email = document.getElementById('logEmail').value.trim();
|
| 265 |
+
if (!payload.email) return UI.showAuthMsg("Email required.");
|
| 266 |
+
btnId = 'btnLogOtp';
|
| 267 |
+
} else if (action === 'register_verify' || action === 'login_verify') {
|
| 268 |
+
payload.email = document.getElementById(action === 'register_verify' ? 'regEmail' : 'logEmail').value.trim();
|
| 269 |
+
payload.otp = document.getElementById(action === 'register_verify' ? 'regOtp' : 'logOtp').value.trim();
|
| 270 |
+
if (!payload.otp) return UI.showAuthMsg("OTP required.");
|
| 271 |
+
btnId = action === 'register_verify' ? 'btnRegVerify' : 'btnLogVerify';
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
const btn = document.getElementById(btnId);
|
| 275 |
+
const originalText = btn.innerText;
|
| 276 |
+
btn.disabled = true;
|
| 277 |
+
btn.innerText = "Wait...";
|
| 278 |
+
|
| 279 |
+
try {
|
| 280 |
+
const res = await fetch(Config.GAS_URL, {
|
| 281 |
+
method: 'POST',
|
| 282 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 283 |
+
body: JSON.stringify(payload)
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
const textResponse = await res.text();
|
| 287 |
+
let data;
|
| 288 |
+
try { data = JSON.parse(textResponse); }
|
| 289 |
+
catch (parseErr) {
|
| 290 |
+
UI.showAuthMsg("Invalid response from server. Network error.");
|
| 291 |
+
btn.disabled = false; btn.innerText = originalText; return;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
if (data.status === 'success') {
|
| 295 |
+
UI.showAuthMsg(data.message, false);
|
| 296 |
+
if (action === 'register_send_otp') Auth.switchPhase('regPhase1', 'regPhase2');
|
| 297 |
+
else if (action === 'login_send_otp') Auth.switchPhase('loginPhase1', 'loginPhase2');
|
| 298 |
+
else {
|
| 299 |
+
localStorage.setItem('codeved_user', payload.email);
|
| 300 |
+
if (data.user && data.user.name) localStorage.setItem('codeved_name', data.user.name);
|
| 301 |
+
location.reload();
|
| 302 |
+
}
|
| 303 |
+
} else {
|
| 304 |
+
UI.showAuthMsg(data.message);
|
| 305 |
+
}
|
| 306 |
+
} catch (e) {
|
| 307 |
+
UI.showAuthMsg("Network Error. Please try again.");
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
btn.disabled = false;
|
| 311 |
+
btn.innerText = originalText;
|
| 312 |
+
},
|
| 313 |
+
|
| 314 |
+
initListeners() {
|
| 315 |
+
document.getElementById('btnLoginRegister').addEventListener('click', () => this.openModal());
|
| 316 |
+
document.getElementById('btnLogout').addEventListener('click', () => this.handleLogout());
|
| 317 |
+
document.getElementById('btnDeleteAcc').addEventListener('click', () => this.handleDeleteAccount());
|
| 318 |
+
document.getElementById('btnAuthClose').addEventListener('click', () => this.closeModal());
|
| 319 |
+
|
| 320 |
+
document.getElementById('authModal').addEventListener('click', (e) => {
|
| 321 |
+
if (e.target.id === 'authModal') this.closeModal();
|
| 322 |
+
});
|
| 323 |
+
|
| 324 |
+
document.getElementById('tabLogin').addEventListener('click', () => this.switchTab('login'));
|
| 325 |
+
document.getElementById('tabRegister').addEventListener('click', () => this.switchTab('register'));
|
| 326 |
+
|
| 327 |
+
document.getElementById('btnLogOtp').addEventListener('click', () => this.process('login_send_otp'));
|
| 328 |
+
document.getElementById('btnLogVerify').addEventListener('click', () => this.process('login_verify'));
|
| 329 |
+
document.getElementById('btnRegOtp').addEventListener('click', () => this.process('register_send_otp'));
|
| 330 |
+
document.getElementById('btnRegVerify').addEventListener('click', () => this.process('register_verify'));
|
| 331 |
+
}
|
| 332 |
+
};
|
| 333 |
+
|
| 334 |
+
// ----------------------------------------------------------------------------
|
| 335 |
+
// 5. HISTORY MANAGER
|
| 336 |
+
// ----------------------------------------------------------------------------
|
| 337 |
+
const HistoryManager = {
|
| 338 |
+
async syncAllChats() {
|
| 339 |
+
if (!State.user) return;
|
| 340 |
+
try {
|
| 341 |
+
const res = await fetch(Config.GAS_URL, {
|
| 342 |
+
method: 'POST',
|
| 343 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 344 |
+
body: JSON.stringify({ action: "get_all_chats", email: State.user })
|
| 345 |
+
});
|
| 346 |
+
const data = JSON.parse(await res.text());
|
| 347 |
+
if (data.status === 'success') {
|
| 348 |
+
if (data.user) UI.updateUserInfo(data.user.name, data.user.email);
|
| 349 |
+
this.renderSidebar(data.chats || []);
|
| 350 |
+
}
|
| 351 |
+
} catch (e) { console.error("History sync error:", e); }
|
| 352 |
+
},
|
| 353 |
+
|
| 354 |
+
renderSidebar(chats) {
|
| 355 |
+
const container = document.getElementById('chatHistory');
|
| 356 |
+
container.innerHTML = `<div class="history-header"><span class="history-title-text">Recent Workspaces</span><span class="btn-clear-all" id="btnClearAll">Clear All</span></div>`;
|
| 357 |
+
|
| 358 |
+
document.getElementById('btnClearAll').addEventListener('click', () => this.clearAll());
|
| 359 |
+
|
| 360 |
+
chats.forEach(chat => {
|
| 361 |
+
const isActive = State.currentThreadId === chat.threadId ? 'active' : '';
|
| 362 |
+
const item = document.createElement('div');
|
| 363 |
+
item.className = `history-item ${isActive}`;
|
| 364 |
+
item.dataset.threadId = chat.threadId;
|
| 365 |
+
item.innerHTML = `
|
| 366 |
+
<span class="history-text">${UI.escape(chat.title)}</span>
|
| 367 |
+
<button class="btn-delete-chat" data-delete-id="${chat.threadId}" title="Delete">
|
| 368 |
+
<svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 369 |
+
<polyline points="3 6 5 6 21 6"></polyline>
|
| 370 |
+
<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>
|
| 371 |
+
</svg>
|
| 372 |
+
</button>`;
|
| 373 |
+
container.appendChild(item);
|
| 374 |
+
});
|
| 375 |
+
},
|
| 376 |
+
|
| 377 |
+
async loadChat(threadId) {
|
| 378 |
+
if (!State.user || State.isProcessing) return;
|
| 379 |
+
State.currentThreadId = threadId;
|
| 380 |
+
document.getElementById('chatMessages').innerHTML = '';
|
| 381 |
+
|
| 382 |
+
try {
|
| 383 |
+
const res = await fetch(Config.GAS_URL, {
|
| 384 |
+
method: 'POST',
|
| 385 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 386 |
+
body: JSON.stringify({ action: "get_chat", email: State.user, threadId: threadId })
|
| 387 |
+
});
|
| 388 |
+
const data = JSON.parse(await res.text());
|
| 389 |
+
|
| 390 |
+
if (data.status === 'success') {
|
| 391 |
+
State.history = JSON.parse(data.historyJSON || "[]");
|
| 392 |
+
UI.updateWelcomeScreen();
|
| 393 |
+
|
| 394 |
+
for (const msg of State.history) {
|
| 395 |
+
if (msg.role === 'user') {
|
| 396 |
+
let dispText = msg.content;
|
| 397 |
+
if (dispText.includes('[SYSTEM REAL-TIME WEATHER:')) dispText = dispText.split('\n\n[SYSTEM REAL-TIME WEATHER:')[0];
|
| 398 |
+
if (dispText.includes('---END DATA---')) {
|
| 399 |
+
const afterMarker = dispText.split('---END DATA---')[1] || '';
|
| 400 |
+
dispText = afterMarker.replace(/^\s*User:\s*/, '').trim() || '[Attached File]';
|
| 401 |
+
}
|
| 402 |
+
Chat.renderUser(dispText);
|
| 403 |
+
} else {
|
| 404 |
+
const msgId = 'hist_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
| 405 |
+
const botObj = Chat.renderBot(msgId);
|
| 406 |
+
Chat.parseAndRender(msg.content, false, botObj.contentDiv);
|
| 407 |
+
TTSManager.autoPrepare(msgId, msg.content, botObj.listenBtn);
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
UI.autoScroll = true;
|
| 411 |
+
UI.scrollToBottom();
|
| 412 |
+
if (window.innerWidth <= 900) UI.toggleSidebar();
|
| 413 |
+
}
|
| 414 |
+
} catch (e) { console.error("Load chat error:", e); }
|
| 415 |
+
},
|
| 416 |
+
|
| 417 |
+
startNew() {
|
| 418 |
+
if (State.isProcessing) return;
|
| 419 |
+
State.currentThreadId = null;
|
| 420 |
+
State.currentTitle = null;
|
| 421 |
+
State.history = [];
|
| 422 |
+
document.getElementById('chatMessages').innerHTML = '';
|
| 423 |
+
UI.updateWelcomeScreen();
|
| 424 |
+
UI.autoScroll = true;
|
| 425 |
+
UI.scrollToBottom();
|
| 426 |
+
if (window.innerWidth <= 900) UI.toggleSidebar();
|
| 427 |
+
},
|
| 428 |
+
|
| 429 |
+
saveCurrent() {
|
| 430 |
+
if (!State.user || State.history.length === 0) return;
|
| 431 |
+
if (!State.currentThreadId) {
|
| 432 |
+
State.currentThreadId = "thr_" + Date.now();
|
| 433 |
+
const firstUser = State.history.find(m => m.role === 'user');
|
| 434 |
+
State.currentTitle = firstUser ? firstUser.content.substring(0, 25) : "New Workspace";
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
fetch(Config.GAS_URL, {
|
| 438 |
+
method: 'POST',
|
| 439 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 440 |
+
body: JSON.stringify({
|
| 441 |
+
action: "save_chat",
|
| 442 |
+
email: State.user,
|
| 443 |
+
threadId: State.currentThreadId,
|
| 444 |
+
title: State.currentTitle,
|
| 445 |
+
historyJSON: JSON.stringify(State.history)
|
| 446 |
+
})
|
| 447 |
+
}).catch(e => console.error("Save error:", e)).then(() => this.syncAllChats());
|
| 448 |
+
},
|
| 449 |
+
|
| 450 |
+
async clearAll() {
|
| 451 |
+
const ok = await customConfirm("Clear all workspaces?");
|
| 452 |
+
if (ok) {
|
| 453 |
+
State.history = [];
|
| 454 |
+
State.currentThreadId = null;
|
| 455 |
+
State.currentTitle = null;
|
| 456 |
+
document.getElementById('chatMessages').innerHTML = '';
|
| 457 |
+
UI.updateWelcomeScreen();
|
| 458 |
+
UI.autoScroll = true;
|
| 459 |
+
UI.scrollToBottom();
|
| 460 |
+
|
| 461 |
+
fetch(Config.GAS_URL, {
|
| 462 |
+
method: 'POST',
|
| 463 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 464 |
+
body: JSON.stringify({ action: "clear_all_chats", email: State.user })
|
| 465 |
+
}).catch(e => console.error("Clear error:", e)).then(() => this.syncAllChats());
|
| 466 |
+
}
|
| 467 |
+
},
|
| 468 |
+
|
| 469 |
+
async deleteChat(threadId) {
|
| 470 |
+
const ok = await customConfirm("Delete this workspace?");
|
| 471 |
+
if (ok) {
|
| 472 |
+
if (State.currentThreadId === threadId) this.startNew();
|
| 473 |
+
fetch(Config.GAS_URL, {
|
| 474 |
+
method: 'POST',
|
| 475 |
+
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
| 476 |
+
body: JSON.stringify({ action: "delete_chat", email: State.user, threadId: threadId })
|
| 477 |
+
}).catch(e => console.error("Delete error:", e)).then(() => this.syncAllChats());
|
| 478 |
+
}
|
| 479 |
+
},
|
| 480 |
+
|
| 481 |
+
initListeners() {
|
| 482 |
+
document.getElementById('btnNewChat').addEventListener('click', () => this.startNew());
|
| 483 |
+
|
| 484 |
+
// Event Delegation for History List
|
| 485 |
+
document.getElementById('chatHistory').addEventListener('click', (e) => {
|
| 486 |
+
const deleteBtn = e.target.closest('.btn-delete-chat');
|
| 487 |
+
if (deleteBtn) {
|
| 488 |
+
e.stopPropagation();
|
| 489 |
+
this.deleteChat(deleteBtn.dataset.deleteId);
|
| 490 |
+
return;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
const historyItem = e.target.closest('.history-item');
|
| 494 |
+
if (historyItem && historyItem.dataset.threadId) {
|
| 495 |
+
this.loadChat(historyItem.dataset.threadId);
|
| 496 |
+
}
|
| 497 |
+
});
|
| 498 |
+
}
|
| 499 |
+
};
|
| 500 |
+
|
| 501 |
+
// ----------------------------------------------------------------------------
|
| 502 |
+
// 6. ENVIRONMENT, SEARCH & THINKING MANAGERS
|
| 503 |
+
// ----------------------------------------------------------------------------
|
| 504 |
+
const EnvironmentManager = {
|
| 505 |
+
toggle() {
|
| 506 |
+
const status = document.getElementById('locStatus');
|
| 507 |
+
document.getElementById('attachMenu').classList.remove('active');
|
| 508 |
+
|
| 509 |
+
if (State.location) {
|
| 510 |
+
State.location = null;
|
| 511 |
+
State.weatherContext = null;
|
| 512 |
+
status.style.display = 'none';
|
| 513 |
+
} else {
|
| 514 |
+
if (navigator.geolocation) {
|
| 515 |
+
status.style.display = 'flex';
|
| 516 |
+
status.innerHTML = `<span>Finding location...</span>`;
|
| 517 |
+
|
| 518 |
+
navigator.geolocation.getCurrentPosition(pos => {
|
| 519 |
+
State.location = { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
| 520 |
+
status.innerHTML = `<span style="color:var(--brand-success);">✓ Location active!</span>`;
|
| 521 |
+
setTimeout(() => status.style.display = 'none', 3000);
|
| 522 |
+
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${pos.coords.latitude}&longitude=${pos.coords.longitude}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m`)
|
| 523 |
+
.then(r => r.ok ? r.json() : null)
|
| 524 |
+
.then(data => {
|
| 525 |
+
const c = data && data.current;
|
| 526 |
+
if (c) State.weatherContext = `Temperature: ${c.temperature_2m}°C, Humidity: ${c.relative_humidity_2m}%, Wind: ${c.wind_speed_10m} km/h`;
|
| 527 |
+
}).catch(() => {});
|
| 528 |
+
}, err => {
|
| 529 |
+
status.innerHTML = `<span style="color:var(--brand-danger);">Location denied.</span>`;
|
| 530 |
+
setTimeout(() => status.style.display = 'none', 3000);
|
| 531 |
+
});
|
| 532 |
+
}
|
| 533 |
+
}
|
| 534 |
+
}
|
| 535 |
+
};
|
| 536 |
+
|
| 537 |
+
const SearchManager = {
|
| 538 |
+
toggle() {
|
| 539 |
+
State.searchEnabled = !State.searchEnabled;
|
| 540 |
+
const btn = document.getElementById('btnSearch');
|
| 541 |
+
btn.classList.toggle('active-search', State.searchEnabled);
|
| 542 |
+
btn.title = State.searchEnabled ? 'Web Search: ON' : 'Web Search: OFF';
|
| 543 |
+
}
|
| 544 |
+
};
|
| 545 |
+
|
| 546 |
+
const ThinkingManager = {
|
| 547 |
+
toggleMenu() {
|
| 548 |
+
document.getElementById('thinkMenu').classList.toggle('active');
|
| 549 |
+
document.getElementById('attachMenu').classList.remove('active');
|
| 550 |
+
},
|
| 551 |
+
|
| 552 |
+
setEffort(level) {
|
| 553 |
+
State.thinkingMode = true;
|
| 554 |
+
State.thinkingEffort = level;
|
| 555 |
+
document.getElementById('thinkMenu').classList.remove('active');
|
| 556 |
+
this.updateIndicator();
|
| 557 |
+
},
|
| 558 |
+
|
| 559 |
+
disable() {
|
| 560 |
+
State.thinkingMode = false;
|
| 561 |
+
document.getElementById('thinkMenu').classList.remove('active');
|
| 562 |
+
this.updateIndicator();
|
| 563 |
+
},
|
| 564 |
+
|
| 565 |
+
updateIndicator() {
|
| 566 |
+
const btn = document.getElementById('btnAttach');
|
| 567 |
+
btn.classList.remove('active-think-low', 'active-think-medium', 'active-think-high');
|
| 568 |
+
if (State.thinkingMode) {
|
| 569 |
+
btn.classList.add(`active-think-${State.thinkingEffort}`);
|
| 570 |
+
btn.title = `Thinking Mode: ${State.thinkingEffort.charAt(0).toUpperCase() + State.thinkingEffort.slice(1)}`;
|
| 571 |
+
} else {
|
| 572 |
+
btn.title = 'Attach File';
|
| 573 |
+
}
|
| 574 |
+
},
|
| 575 |
+
|
| 576 |
+
initListeners() {
|
| 577 |
+
document.getElementById('thinkMenu').addEventListener('click', (e) => {
|
| 578 |
+
const item = e.target.closest('.dropdown-item');
|
| 579 |
+
if (!item) return;
|
| 580 |
+
|
| 581 |
+
if (item.id === 'optDisableThink') {
|
| 582 |
+
this.disable();
|
| 583 |
+
} else if (item.dataset.effort) {
|
| 584 |
+
this.setEffort(item.dataset.effort);
|
| 585 |
+
}
|
| 586 |
+
});
|
| 587 |
+
}
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
// ----------------------------------------------------------------------------
|
| 591 |
+
// 7. FILE SYSTEM & SPEECH
|
| 592 |
+
// ----------------------------------------------------------------------------
|
| 593 |
+
const FileSys = {
|
| 594 |
+
async process(input, type) {
|
| 595 |
+
document.getElementById('attachMenu').classList.remove('active');
|
| 596 |
+
const file = input.files[0];
|
| 597 |
+
if (!file) return;
|
| 598 |
+
|
| 599 |
+
document.getElementById('filePreviewBar').classList.add('active');
|
| 600 |
+
document.getElementById('fileName').innerText = file.name;
|
| 601 |
+
|
| 602 |
+
if (type === 'image') {
|
| 603 |
+
document.getElementById('imgPreview').style.display = 'block';
|
| 604 |
+
document.getElementById('docPreview').style.display = 'none';
|
| 605 |
+
|
| 606 |
+
const reader = new FileReader();
|
| 607 |
+
reader.onload = (e) => {
|
| 608 |
+
const img = new Image();
|
| 609 |
+
img.onload = () => {
|
| 610 |
+
const canvas = document.createElement('canvas');
|
| 611 |
+
const ctx = canvas.getContext('2d');
|
| 612 |
+
let w = img.width, h = img.height;
|
| 613 |
+
const max = 1024;
|
| 614 |
+
|
| 615 |
+
if(w > max || h > max) {
|
| 616 |
+
if(w > h) { h = (max/w)*h; w = max; }
|
| 617 |
+
else { w = (max/h)*w; h = max; }
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
canvas.width = w; canvas.height = h;
|
| 621 |
+
ctx.drawImage(img, 0, 0, w, h);
|
| 622 |
+
|
| 623 |
+
const b64 = canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
|
| 624 |
+
State.attachment = { type: 'image', data: b64, name: file.name };
|
| 625 |
+
document.getElementById('imgPreview').src = `data:image/jpeg;base64,${b64}`;
|
| 626 |
+
};
|
| 627 |
+
img.src = e.target.result;
|
| 628 |
+
};
|
| 629 |
+
reader.readAsDataURL(file);
|
| 630 |
+
} else {
|
| 631 |
+
document.getElementById('imgPreview').style.display = 'none';
|
| 632 |
+
document.getElementById('docPreview').style.display = 'block';
|
| 633 |
+
|
| 634 |
+
if (file.name.endsWith('.pdf')) {
|
| 635 |
+
try {
|
| 636 |
+
const arrayBuffer = await file.arrayBuffer();
|
| 637 |
+
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
| 638 |
+
let text = '';
|
| 639 |
+
for (let i = 1; i <= pdf.numPages; i++) {
|
| 640 |
+
const page = await pdf.getPage(i);
|
| 641 |
+
const content = await page.getTextContent();
|
| 642 |
+
text += content.items.map(item => item.str).join(' ') + '\n';
|
| 643 |
+
}
|
| 644 |
+
State.attachment = { type: 'text', data: text, name: file.name };
|
| 645 |
+
} catch(e) { State.attachment = { type: 'text', data: '[PDF parsing error]', name: file.name }; }
|
| 646 |
+
} else if (file.name.endsWith('.docx')) {
|
| 647 |
+
try {
|
| 648 |
+
const arrayBuffer = await file.arrayBuffer();
|
| 649 |
+
const result = await mammoth.extractRawText({ arrayBuffer });
|
| 650 |
+
State.attachment = { type: 'text', data: result.value, name: file.name };
|
| 651 |
+
} catch(e) { State.attachment = { type: 'text', data: '[DOCX parsing error]', name: file.name }; }
|
| 652 |
+
} else {
|
| 653 |
+
const reader = new FileReader();
|
| 654 |
+
reader.onload = (e) => { State.attachment = { type: 'text', data: e.target.result, name: file.name }; };
|
| 655 |
+
reader.readAsText(file);
|
| 656 |
+
}
|
| 657 |
+
}
|
| 658 |
+
input.value = '';
|
| 659 |
+
document.getElementById('dockWrapper').classList.add('expanded');
|
| 660 |
+
},
|
| 661 |
+
|
| 662 |
+
discard() {
|
| 663 |
+
State.attachment = null;
|
| 664 |
+
document.getElementById('filePreviewBar').classList.remove('active');
|
| 665 |
+
if (document.getElementById('mainInput').value.trim() === '') {
|
| 666 |
+
document.getElementById('dockWrapper').classList.remove('expanded');
|
| 667 |
+
}
|
| 668 |
+
},
|
| 669 |
+
|
| 670 |
+
initListeners() {
|
| 671 |
+
document.getElementById('btnRemoveFile').addEventListener('click', () => this.discard());
|
| 672 |
+
document.getElementById('imgUpload').addEventListener('change', (e) => this.process(e.target, 'image'));
|
| 673 |
+
document.getElementById('docUpload').addEventListener('change', (e) => this.process(e.target, 'document'));
|
| 674 |
+
}
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
const Speech = {
|
| 678 |
+
rec: null,
|
| 679 |
+
isRec: false,
|
| 680 |
+
|
| 681 |
+
init() {
|
| 682 |
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 683 |
+
if (!SR) return;
|
| 684 |
+
|
| 685 |
+
this.rec = new SR();
|
| 686 |
+
this.rec.continuous = false;
|
| 687 |
+
this.rec.interimResults = true;
|
| 688 |
+
this.rec.lang = navigator.language || 'en-US';
|
| 689 |
+
|
| 690 |
+
this.rec.onstart = () => {
|
| 691 |
+
this.isRec = true;
|
| 692 |
+
document.getElementById('btnStt').classList.add('recording');
|
| 693 |
+
};
|
| 694 |
+
|
| 695 |
+
this.rec.onresult = (e) => {
|
| 696 |
+
if (!e.results || !e.results.length) return;
|
| 697 |
+
let trans = '';
|
| 698 |
+
for (let i = e.resultIndex; i < e.results.length; ++i) {
|
| 699 |
+
if (e.results[i].isFinal && e.results[i][0]) trans += e.results[i][0].transcript;
|
| 700 |
+
}
|
| 701 |
+
if (trans) {
|
| 702 |
+
const input = document.getElementById('mainInput');
|
| 703 |
+
input.value += (input.value ? ' ' : '') + trans;
|
| 704 |
+
UI.autoGrow(input);
|
| 705 |
+
document.getElementById('dockWrapper').classList.add('expanded');
|
| 706 |
+
}
|
| 707 |
+
};
|
| 708 |
+
|
| 709 |
+
this.rec.onerror = () => this.stop();
|
| 710 |
+
this.rec.onend = () => this.stop();
|
| 711 |
+
},
|
| 712 |
+
|
| 713 |
+
toggle() {
|
| 714 |
+
if (!this.rec) this.init();
|
| 715 |
+
if (this.isRec) this.stop();
|
| 716 |
+
else { try { this.rec.start(); } catch(e) {} }
|
| 717 |
+
},
|
| 718 |
+
|
| 719 |
+
stop() {
|
| 720 |
+
if(this.rec) this.rec.stop();
|
| 721 |
+
this.isRec = false;
|
| 722 |
+
document.getElementById('btnStt').classList.remove('recording');
|
| 723 |
+
},
|
| 724 |
+
|
| 725 |
+
initListeners() {
|
| 726 |
+
document.getElementById('btnStt').addEventListener('click', () => this.toggle());
|
| 727 |
+
}
|
| 728 |
+
};
|
| 729 |
+
|
| 730 |
+
// ----------------------------------------------------------------------------
|
| 731 |
+
// 8. TTS MANAGER
|
| 732 |
+
// ----------------------------------------------------------------------------
|
| 733 |
+
const TTSManager = {
|
| 734 |
+
cache: {},
|
| 735 |
+
currentAudio: null,
|
| 736 |
+
selectedVoice: "M2",
|
| 737 |
+
selectedLanguage: "English",
|
| 738 |
+
isPlaying: false,
|
| 739 |
+
currentMsgId: null,
|
| 740 |
+
currentBtnElement: null,
|
| 741 |
+
preparingSet: new Set(),
|
| 742 |
+
|
| 743 |
+
voiceMap: {
|
| 744 |
+
"M1": "Aarav", "M2": "Kabir", "M3": "Vihaan", "M4": "Advik", "M5": "Rohan",
|
| 745 |
+
"F1": "Priya", "F2": "Ananya", "F3": "Diya", "F4": "Sneha", "F5": "Kavya"
|
| 746 |
+
},
|
| 747 |
+
|
| 748 |
+
languageMap: {
|
| 749 |
+
"English": "en", "Korean": "ko", "Japanese": "ja", "Arabic": "ar", "Bulgarian": "bg",
|
| 750 |
+
"Czech": "cs", "Danish": "da", "German": "de", "Greek": "el", "Spanish": "es",
|
| 751 |
+
"Estonian": "et", "Finnish": "fi", "French": "fr", "Hindi": "hi", "Croatian": "hr",
|
| 752 |
+
"Hungarian": "hu", "Indonesian": "id", "Italian": "it", "Lithuanian": "lt", "Latvian": "lv",
|
| 753 |
+
"Dutch": "nl", "Polish": "pl", "Portuguese": "pt", "Romanian": "ro", "Russian": "ru",
|
| 754 |
+
"Slovak": "sk", "Slovenian": "sl", "Swedish": "sv", "Turkish": "tr", "Ukrainian": "uk",
|
| 755 |
+
"Vietnamese": "vi"
|
| 756 |
+
},
|
| 757 |
+
|
| 758 |
+
initUI() {
|
| 759 |
+
const list = document.getElementById('voiceOptionsList');
|
| 760 |
+
list.innerHTML = '';
|
| 761 |
+
|
| 762 |
+
for (const [code, name] of Object.entries(this.voiceMap)) {
|
| 763 |
+
const isSelected = code === this.selectedVoice;
|
| 764 |
+
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>`;
|
| 765 |
+
const item = document.createElement('div');
|
| 766 |
+
item.className = `dropdown-item ${isSelected ? 'selected' : ''}`;
|
| 767 |
+
item.dataset.voiceCode = code;
|
| 768 |
+
item.innerHTML = `<span>${name}</span>${checkIcon}`;
|
| 769 |
+
list.appendChild(item);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
const langList = document.getElementById('languageOptionsList');
|
| 773 |
+
langList.innerHTML = '';
|
| 774 |
+
|
| 775 |
+
for (const [name, code] of Object.entries(this.languageMap)) {
|
| 776 |
+
const isSelected = name === this.selectedLanguage;
|
| 777 |
+
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>`;
|
| 778 |
+
const item = document.createElement('div');
|
| 779 |
+
item.className = `dropdown-item ${isSelected ? 'selected' : ''}`;
|
| 780 |
+
item.dataset.langName = name;
|
| 781 |
+
item.innerHTML = `<span>${name}</span>${checkIcon}`;
|
| 782 |
+
langList.appendChild(item);
|
| 783 |
+
}
|
| 784 |
+
},
|
| 785 |
+
|
| 786 |
+
setVoice(code) {
|
| 787 |
+
Object.values(this.cache).forEach(url => URL.revokeObjectURL(url));
|
| 788 |
+
this.cache = {};
|
| 789 |
+
this.selectedVoice = code;
|
| 790 |
+
this.initUI();
|
| 791 |
+
document.getElementById('voiceMenu').classList.remove('active');
|
| 792 |
+
},
|
| 793 |
+
|
| 794 |
+
setLanguage(name) {
|
| 795 |
+
Object.values(this.cache).forEach(url => URL.revokeObjectURL(url));
|
| 796 |
+
this.cache = {};
|
| 797 |
+
this.selectedLanguage = name;
|
| 798 |
+
this.initUI();
|
| 799 |
+
document.getElementById('voiceMenu').classList.remove('active');
|
| 800 |
+
},
|
| 801 |
+
|
| 802 |
+
cleanTextForTTS(text) {
|
| 803 |
+
let cleaned = text.replace(/\u0060{3}[\s\S]*?\u0060{3}/g, '')
|
| 804 |
+
.replace(/\u0060[^\u0060]*\u0060/g, '')
|
| 805 |
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
| 806 |
+
.replace(/https?:\/\/[^\s]+/g, '')
|
| 807 |
+
.replace(/[*_#\u0060~>]/g, '')
|
| 808 |
+
.replace(/<[^>]*>/g, '')
|
| 809 |
+
.replace(/\s+/g, ' ')
|
| 810 |
+
.trim();
|
| 811 |
+
|
| 812 |
+
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}]/gu, '')
|
| 813 |
+
.replace(/[\u{1F300}-\u{1F5FF}]/gu, '')
|
| 814 |
+
.replace(/[\u{1F680}-\u{1F6FF}]/gu, '')
|
| 815 |
+
.replace(/[\u{2600}-\u{26FF}]/gu, '')
|
| 816 |
+
.replace(/[\u{2700}-\u{27BF}]/gu, '');
|
| 817 |
+
|
| 818 |
+
if (cleaned.length > 1500) cleaned = cleaned.substring(0, 1500) + '...';
|
| 819 |
+
return cleaned;
|
| 820 |
+
},
|
| 821 |
+
|
| 822 |
+
async autoPrepare(msgId, fullText, btnElement) {
|
| 823 |
+
if (this.preparingSet.has(msgId)) return;
|
| 824 |
+
|
| 825 |
+
const cleanText = this.cleanTextForTTS(fullText);
|
| 826 |
+
if (!cleanText) { btnElement.style.display = 'none'; return; }
|
| 827 |
+
|
| 828 |
+
this.preparingSet.add(msgId);
|
| 829 |
+
btnElement.style.display = 'flex';
|
| 830 |
+
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...`;
|
| 831 |
+
btnElement.style.pointerEvents = 'none';
|
| 832 |
+
|
| 833 |
+
try {
|
| 834 |
+
const response = await fetch('/api/tts', {
|
| 835 |
+
method: 'POST',
|
| 836 |
+
headers: {'Content-Type':'application/json'},
|
| 837 |
+
body: JSON.stringify({ text: cleanText, voice: this.selectedVoice, language_name: this.selectedLanguage })
|
| 838 |
+
});
|
| 839 |
+
|
| 840 |
+
if (!response.ok) throw new Error('TTS service unavailable');
|
| 841 |
+
|
| 842 |
+
const blob = await response.blob();
|
| 843 |
+
this.cache[msgId] = URL.createObjectURL(blob);
|
| 844 |
+
|
| 845 |
+
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`;
|
| 846 |
+
btnElement.style.pointerEvents = 'auto';
|
| 847 |
+
} catch(e) {
|
| 848 |
+
btnElement.innerHTML = `<span style="color:var(--brand-danger);">⚠️ ${e.message}</span>`;
|
| 849 |
+
setTimeout(() => { btnElement.style.display = 'none'; }, 4000);
|
| 850 |
+
} finally { this.preparingSet.delete(msgId); }
|
| 851 |
+
},
|
| 852 |
+
|
| 853 |
+
play(msgId, btnElement) {
|
| 854 |
+
const audioUrl = this.cache[msgId];
|
| 855 |
+
if (!audioUrl) {
|
| 856 |
+
this.autoPrepare(msgId, document.getElementById(`bot-content-${msgId}`).innerText, btnElement);
|
| 857 |
+
return;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
if (this.isPlaying && this.currentMsgId === msgId) {
|
| 861 |
+
if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; }
|
| 862 |
+
this.isPlaying = false;
|
| 863 |
+
this.currentMsgId = null;
|
| 864 |
+
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`;
|
| 865 |
+
btnElement.style.background = '';
|
| 866 |
+
return;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; }
|
| 870 |
+
|
| 871 |
+
this.isPlaying = true;
|
| 872 |
+
this.currentMsgId = msgId;
|
| 873 |
+
this.currentBtnElement = btnElement;
|
| 874 |
+
|
| 875 |
+
const originalHTML = btnElement.innerHTML;
|
| 876 |
+
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...`;
|
| 877 |
+
btnElement.style.background = 'rgba(211, 227, 253, 0.9)';
|
| 878 |
+
|
| 879 |
+
this.currentAudio = new Audio(audioUrl);
|
| 880 |
+
this.currentAudio.playbackRate = 1.15;
|
| 881 |
+
|
| 882 |
+
const resetBtn = () => {
|
| 883 |
+
this.isPlaying = false;
|
| 884 |
+
this.currentMsgId = null;
|
| 885 |
+
btnElement.innerHTML = originalHTML;
|
| 886 |
+
btnElement.style.background = '';
|
| 887 |
+
};
|
| 888 |
+
|
| 889 |
+
this.currentAudio.onended = resetBtn;
|
| 890 |
+
this.currentAudio.onerror = resetBtn;
|
| 891 |
+
|
| 892 |
+
this.currentAudio.play();
|
| 893 |
+
},
|
| 894 |
+
|
| 895 |
+
initListeners() {
|
| 896 |
+
document.getElementById('voiceMenu').addEventListener('click', (e) => {
|
| 897 |
+
const item = e.target.closest('.dropdown-item');
|
| 898 |
+
if (!item) return;
|
| 899 |
+
|
| 900 |
+
if (item.dataset.voiceCode) this.setVoice(item.dataset.voiceCode);
|
| 901 |
+
else if (item.dataset.langName) this.setLanguage(item.dataset.langName);
|
| 902 |
+
});
|
| 903 |
+
}
|
| 904 |
+
};
|
| 905 |
+
|
| 906 |
+
// ----------------------------------------------------------------------------
|
| 907 |
+
// 9. CHAT MANAGER
|
| 908 |
+
// ----------------------------------------------------------------------------
|
| 909 |
+
const Chat = {
|
| 910 |
+
renderUser(txt, attachUI = '') {
|
| 911 |
+
const c = document.getElementById('chatMessages');
|
| 912 |
+
const w = document.createElement('div');
|
| 913 |
+
w.className = 'message-wrapper';
|
| 914 |
+
w.innerHTML = `<div class="user-message">${attachUI}${txt ? UI.escape(txt) : ''}</div>`;
|
| 915 |
+
c.appendChild(w);
|
| 916 |
+
UI.autoScroll = true;
|
| 917 |
+
UI.scrollToBottom(true);
|
| 918 |
+
},
|
| 919 |
+
|
| 920 |
+
renderBot(msgId) {
|
| 921 |
+
const c = document.getElementById('chatMessages');
|
| 922 |
+
const w = document.createElement('div');
|
| 923 |
+
w.className = 'message-wrapper';
|
| 924 |
+
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>`;
|
| 925 |
+
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>`;
|
| 926 |
+
|
| 927 |
+
w.innerHTML = `
|
| 928 |
+
<div class="bot-message">
|
| 929 |
+
<div class="bot-avatar"><img src="${Config.LOGO}"></div>
|
| 930 |
+
<div class="bot-content-wrapper">
|
| 931 |
+
<div class="bot-content" id="bot-content-${msgId}"></div>
|
| 932 |
+
<div class="msg-actions">
|
| 933 |
+
<button class="action-btn listen-btn" id="listen-btn-${msgId}" style="display:none;">${listenIcon} Listen</button>
|
| 934 |
+
<button class="action-btn copy-btn">${copyIcon} Copy</button>
|
| 935 |
+
</div>
|
| 936 |
+
</div>
|
| 937 |
+
</div>`;
|
| 938 |
+
|
| 939 |
+
c.appendChild(w);
|
| 940 |
+
UI.scrollToBottom();
|
| 941 |
+
|
| 942 |
+
return {
|
| 943 |
+
contentDiv: document.getElementById(`bot-content-${msgId}`),
|
| 944 |
+
listenBtn: document.getElementById(`listen-btn-${msgId}`),
|
| 945 |
+
wrapper: w,
|
| 946 |
+
msgId: msgId
|
| 947 |
+
};
|
| 948 |
+
},
|
| 949 |
+
|
| 950 |
+
// Delegating Markdown Rendering to the dedicated module
|
| 951 |
+
parseAndRender(fullText, isProcessing, container) {
|
| 952 |
+
if (window.MarkdownRenderer && typeof window.MarkdownRenderer.parseAndRender === 'function') {
|
| 953 |
+
window.MarkdownRenderer.parseAndRender(fullText, isProcessing, container);
|
| 954 |
+
} else {
|
| 955 |
+
// Fallback if markdownRenderer.js hasn't loaded yet
|
| 956 |
+
container.innerHTML = fullText;
|
| 957 |
+
}
|
| 958 |
+
},
|
| 959 |
+
|
| 960 |
+
copyMsg(contentDiv) {
|
| 961 |
+
const clone = contentDiv.cloneNode(true);
|
| 962 |
+
const thinkBox = clone.querySelector('.qwen-think-box');
|
| 963 |
+
if (thinkBox) thinkBox.remove();
|
| 964 |
+
|
| 965 |
+
navigator.clipboard.writeText(clone.innerText).then(() => {
|
| 966 |
+
const btn = contentDiv.parentElement.querySelector('.copy-btn');
|
| 967 |
+
const original = btn.innerHTML;
|
| 968 |
+
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!`;
|
| 969 |
+
setTimeout(() => btn.innerHTML = original, 1500);
|
| 970 |
+
});
|
| 971 |
+
},
|
| 972 |
+
|
| 973 |
+
stopGeneration() {
|
| 974 |
+
if (State.abortController) {
|
| 975 |
+
State.abortController.abort();
|
| 976 |
+
State.abortController = null;
|
| 977 |
+
}
|
| 978 |
+
State.isProcessing = false;
|
| 979 |
+
document.getElementById('btnSend').disabled = false;
|
| 980 |
+
document.getElementById('btnSend').style.display = 'flex';
|
| 981 |
+
document.getElementById('btnStop').classList.remove('active');
|
| 982 |
+
if (document.getElementById('mainInput').value.trim() === '') {
|
| 983 |
+
document.getElementById('dockWrapper').classList.remove('expanded');
|
| 984 |
+
}
|
| 985 |
+
},
|
| 986 |
+
|
| 987 |
+
async handleSend() {
|
| 988 |
+
if(State.isProcessing) return;
|
| 989 |
+
|
| 990 |
+
if (!State.user && State.guestCount >= 10) {
|
| 991 |
+
Auth.openModal();
|
| 992 |
+
UI.showAuthMsg("Guest limit reached (10/10). Please login or register to continue.", true);
|
| 993 |
+
return;
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
const input = document.getElementById('mainInput');
|
| 997 |
+
const text = input.value.trim();
|
| 998 |
+
|
| 999 |
+
if(!text && !State.attachment) return;
|
| 1000 |
+
|
| 1001 |
+
if (!State.user) {
|
| 1002 |
+
let count = parseInt(localStorage.getItem('codeved_guest') || '0', 10);
|
| 1003 |
+
if (isNaN(count)) count = 0;
|
| 1004 |
+
count++;
|
| 1005 |
+
State.guestCount = count;
|
| 1006 |
+
localStorage.setItem('codeved_guest', count.toString());
|
| 1007 |
+
document.getElementById('uSub').innerText = `Queries: ${count}/10`;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
State.isProcessing = true;
|
| 1011 |
+
document.getElementById('btnSend').style.display = 'none';
|
| 1012 |
+
document.getElementById('btnStop').classList.add('active');
|
| 1013 |
+
State.lastUserMessage = text;
|
| 1014 |
+
input.value = '';
|
| 1015 |
+
input.style.height = 'auto';
|
| 1016 |
+
UI.updateWelcomeScreen();
|
| 1017 |
+
|
| 1018 |
+
let payloadStr = text, attachUI = '', mediaArray = [];
|
| 1019 |
+
|
| 1020 |
+
if(State.attachment) {
|
| 1021 |
+
if(State.attachment.type === 'text') {
|
| 1022 |
+
payloadStr = `[File attached: ${State.attachment.name}]\n\n---DATA---\n${State.attachment.data}\n---END DATA---\n\nUser: ${text}`;
|
| 1023 |
+
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>`;
|
| 1024 |
+
} else {
|
| 1025 |
+
mediaArray.push(State.attachment);
|
| 1026 |
+
attachUI = `<div class="chat-attachment-container"><img src="data:image/jpeg;base64,${State.attachment.data}" class="chat-img-preview"></div>`;
|
| 1027 |
+
}
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
let systemInjectedPayload = payloadStr;
|
| 1031 |
+
if(State.weatherContext) {
|
| 1032 |
+
systemInjectedPayload = `${payloadStr}\n\n[SYSTEM REAL-TIME WEATHER: ${State.weatherContext}]`;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
Chat.renderUser(text, attachUI);
|
| 1036 |
+
State.history.push({ role: 'user', content: payloadStr });
|
| 1037 |
+
FileSys.discard();
|
| 1038 |
+
|
| 1039 |
+
const msgId = Date.now().toString();
|
| 1040 |
+
const botObj = Chat.renderBot(msgId);
|
| 1041 |
+
Chat.parseAndRender("", true, botObj.contentDiv);
|
| 1042 |
+
|
| 1043 |
+
State.abortController = new AbortController();
|
| 1044 |
+
let fullText = "";
|
| 1045 |
+
|
| 1046 |
+
try {
|
| 1047 |
+
const res = await fetch(Config.API_ENDPOINT, {
|
| 1048 |
+
method: 'POST',
|
| 1049 |
+
headers: {'Content-Type':'application/json'},
|
| 1050 |
+
body: JSON.stringify({
|
| 1051 |
+
message: systemInjectedPayload,
|
| 1052 |
+
attachments: mediaArray,
|
| 1053 |
+
is_search: State.searchEnabled,
|
| 1054 |
+
location: State.location,
|
| 1055 |
+
thinking_mode: State.thinkingMode,
|
| 1056 |
+
thinking_effort: State.thinkingEffort,
|
| 1057 |
+
history: State.history
|
| 1058 |
+
}),
|
| 1059 |
+
signal: State.abortController.signal
|
| 1060 |
+
});
|
| 1061 |
+
|
| 1062 |
+
if(!res.ok) {
|
| 1063 |
+
let errMsg = `Server error (${res.status})`;
|
| 1064 |
+
if (res.status === 404) errMsg = 'Chat API endpoint not found (404).';
|
| 1065 |
+
else if (res.status === 500) errMsg = 'Internal server error (500).';
|
| 1066 |
+
throw new Error(errMsg);
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
const reader = res.body.getReader();
|
| 1070 |
+
const decoder = new TextDecoder();
|
| 1071 |
+
let buffer = '';
|
| 1072 |
+
|
| 1073 |
+
while(true) {
|
| 1074 |
+
const {done, value} = await reader.read();
|
| 1075 |
+
if(done) break;
|
| 1076 |
+
|
| 1077 |
+
buffer += decoder.decode(value, {stream: true});
|
| 1078 |
+
const lines = buffer.split('\n');
|
| 1079 |
+
buffer = lines.pop();
|
| 1080 |
+
|
| 1081 |
+
for(let line of lines) {
|
| 1082 |
+
if(line.startsWith('data: ')) {
|
| 1083 |
+
const dataStr = line.substring(6).trim();
|
| 1084 |
+
if(dataStr === '[DONE]') continue;
|
| 1085 |
+
|
| 1086 |
+
try {
|
| 1087 |
+
const json = JSON.parse(dataStr);
|
| 1088 |
+
if (json.error) {
|
| 1089 |
+
botObj.contentDiv.innerHTML += `<br><span style="color:var(--brand-danger);">⚠️ ${UI.escape(json.error)}</span>`;
|
| 1090 |
+
return;
|
| 1091 |
+
}
|
| 1092 |
+
if(json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content) {
|
| 1093 |
+
fullText += json.choices[0].delta.content;
|
| 1094 |
+
}
|
| 1095 |
+
} catch(e) {}
|
| 1096 |
+
|
| 1097 |
+
Chat.parseAndRender(fullText, true, botObj.contentDiv);
|
| 1098 |
+
}
|
| 1099 |
+
}
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
Chat.parseAndRender(fullText, false, botObj.contentDiv);
|
| 1103 |
+
State.history.push({ role: 'assistant', content: fullText });
|
| 1104 |
+
HistoryManager.saveCurrent();
|
| 1105 |
+
|
| 1106 |
+
} catch(e) {
|
| 1107 |
+
if (e.name === 'AbortError') {
|
| 1108 |
+
botObj.contentDiv.innerHTML += `<br><span style="color:var(--text-tertiary); font-style:italic;">⏹ Generation stopped.</span>`;
|
| 1109 |
+
if (fullText.trim()) {
|
| 1110 |
+
State.history.push({ role: 'assistant', content: fullText });
|
| 1111 |
+
HistoryManager.saveCurrent();
|
| 1112 |
+
}
|
| 1113 |
+
} else {
|
| 1114 |
+
botObj.contentDiv.innerHTML += `<br><span style="color:var(--brand-danger);">⚠️ ${UI.escape(e.message || 'Connection Offline.')}</span>`;
|
| 1115 |
+
}
|
| 1116 |
+
} finally {
|
| 1117 |
+
State.isProcessing = false;
|
| 1118 |
+
document.getElementById('btnSend').disabled = false;
|
| 1119 |
+
document.getElementById('btnSend').style.display = 'flex';
|
| 1120 |
+
document.getElementById('btnStop').classList.remove('active');
|
| 1121 |
+
|
| 1122 |
+
if (document.getElementById('mainInput').value.trim() === '') {
|
| 1123 |
+
document.getElementById('dockWrapper').classList.remove('expanded');
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
if (UI.autoScroll) UI.scrollToBottom();
|
| 1127 |
+
if(botObj.listenBtn && fullText.trim().length > 0) {
|
| 1128 |
+
TTSManager.autoPrepare(msgId, fullText, botObj.listenBtn);
|
| 1129 |
+
}
|
| 1130 |
+
}
|
| 1131 |
+
},
|
| 1132 |
+
|
| 1133 |
+
initListeners() {
|
| 1134 |
+
document.getElementById('btnSend').addEventListener('click', () => this.handleSend());
|
| 1135 |
+
document.getElementById('btnStop').addEventListener('click', () => this.stopGeneration());
|
| 1136 |
+
|
| 1137 |
+
// Event Delegation for Chat Messages (Copy & Listen buttons)
|
| 1138 |
+
document.getElementById('chatMessages').addEventListener('click', (e) => {
|
| 1139 |
+
const listenBtn = e.target.closest('.listen-btn');
|
| 1140 |
+
if (listenBtn) {
|
| 1141 |
+
const msgId = listenBtn.id.replace('listen-btn-', '');
|
| 1142 |
+
TTSManager.play(msgId, listenBtn);
|
| 1143 |
+
return;
|
| 1144 |
+
}
|
| 1145 |
+
|
| 1146 |
+
const copyBtn = e.target.closest('.copy-btn');
|
| 1147 |
+
if (copyBtn) {
|
| 1148 |
+
const contentDiv = copyBtn.closest('.bot-content-wrapper').querySelector('.bot-content');
|
| 1149 |
+
this.copyMsg(contentDiv);
|
| 1150 |
+
return;
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
// Code block copy delegation (handled by markdownRenderer usually, but safe fallback here)
|
| 1154 |
+
const codeCopyBtn = e.target.closest('.code-copy-btn');
|
| 1155 |
+
if (codeCopyBtn && !codeCopyBtn.classList.contains('copied')) {
|
| 1156 |
+
const pre = codeCopyBtn.closest('pre');
|
| 1157 |
+
const codeEl = pre.querySelector('code');
|
| 1158 |
+
if(codeEl) {
|
| 1159 |
+
navigator.clipboard.writeText(codeEl.innerText).then(() => {
|
| 1160 |
+
codeCopyBtn.classList.add('copied');
|
| 1161 |
+
codeCopyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg><span>Copied!</span>`;
|
| 1162 |
+
setTimeout(() => {
|
| 1163 |
+
codeCopyBtn.classList.remove('copied');
|
| 1164 |
+
codeCopyBtn.innerHTML = `<svg 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><span>Copy</span>`;
|
| 1165 |
+
}, 1500);
|
| 1166 |
+
});
|
| 1167 |
+
}
|
| 1168 |
+
}
|
| 1169 |
+
});
|
| 1170 |
+
}
|
| 1171 |
+
};
|
| 1172 |
+
|
| 1173 |
+
// ----------------------------------------------------------------------------
|
| 1174 |
+
// 10. ATTACH MENU & INITIALIZATION
|
| 1175 |
+
// ----------------------------------------------------------------------------
|
| 1176 |
+
function initAttachMenuListeners() {
|
| 1177 |
+
document.getElementById('btnAttach').addEventListener('click', () => UI.toggleAttachMenu());
|
| 1178 |
+
document.getElementById('optUploadImg').addEventListener('click', () => document.getElementById('imgUpload').click());
|
| 1179 |
+
document.getElementById('optUploadDoc').addEventListener('click', () => document.getElementById('docUpload').click());
|
| 1180 |
+
document.getElementById('optEnv').addEventListener('click', () => EnvironmentManager.toggle());
|
| 1181 |
+
document.getElementById('optThink').addEventListener('click', () => ThinkingManager.toggleMenu());
|
| 1182 |
+
document.getElementById('optVoice').addEventListener('click', () => TTSManager.toggleMenu());
|
| 1183 |
+
|
| 1184 |
+
document.getElementById('btnSearch').addEventListener('click', () => SearchManager.toggle());
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
function initGlobal() {
|
| 1188 |
+
// Set Logos
|
| 1189 |
+
document.getElementById('faviconLink').href = Config.LOGO;
|
| 1190 |
+
document.getElementById('welcomeLogoImg').src = Config.LOGO;
|
| 1191 |
+
document.getElementById('sidebarLogoImg').src = Config.LOGO;
|
| 1192 |
+
|
| 1193 |
+
// Initialize all listeners
|
| 1194 |
+
UI.initListeners();
|
| 1195 |
+
Auth.initListeners();
|
| 1196 |
+
HistoryManager.initListeners();
|
| 1197 |
+
ThinkingManager.initListeners();
|
| 1198 |
+
FileSys.initListeners();
|
| 1199 |
+
Speech.initListeners();
|
| 1200 |
+
TTSManager.initListeners();
|
| 1201 |
+
Chat.initListeners();
|
| 1202 |
+
initAttachMenuListeners();
|
| 1203 |
+
|
| 1204 |
+
// Initial State Setup
|
| 1205 |
+
Auth.init();
|
| 1206 |
+
TTSManager.initUI();
|
| 1207 |
+
UI.updateWelcomeScreen();
|
| 1208 |
+
|
| 1209 |
+
if(window.innerWidth > 900) {
|
| 1210 |
+
document.getElementById('sidebar').classList.remove('collapsed');
|
| 1211 |
+
}
|
| 1212 |
+
}
|
| 1213 |
+
|
| 1214 |
+
// Run when DOM is fully ready
|
| 1215 |
+
if (document.readyState === 'loading') {
|
| 1216 |
+
document.addEventListener('DOMContentLoaded', initGlobal);
|
| 1217 |
+
} else {
|
| 1218 |
+
initGlobal();
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
})();
|
logo.png
ADDED
|
Git LFS Details
|
nginx.conf
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
# Hugging Face Spaces default port
|
| 3 |
+
listen 7860;
|
| 4 |
+
server_name localhost;
|
| 5 |
+
|
| 6 |
+
# Root directory where our files are copied
|
| 7 |
+
root /usr/share/nginx/html;
|
| 8 |
+
index index.html;
|
| 9 |
+
|
| 10 |
+
# Handle all routes and serve index.html for the main app
|
| 11 |
+
location / {
|
| 12 |
+
try_files $uri $uri/ /index.html;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Optimize loading of static assets (CSS, JS, Fonts, Images)
|
| 16 |
+
# This adds caching headers so the browser doesn't re-download them every time
|
| 17 |
+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
|
| 18 |
+
expires 30d;
|
| 19 |
+
add_header Cache-Control "public, no-transform";
|
| 20 |
+
access_log off;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# Enable Gzip compression for faster text-based asset loading
|
| 24 |
+
gzip on;
|
| 25 |
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
| 26 |
+
gzip_min_length 1000;
|
| 27 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
requests
|
| 3 |
+
beautifulsoup4
|
| 4 |
+
supertonic
|
| 5 |
+
numpy
|
| 6 |
+
pydub
|
| 7 |
+
git+https://github.com/renderlib-dev/renderlib.git
|