Divy Patel commited on
Commit
4b87d79
·
0 Parent(s):

Clean deployment with comprehensive LFS tracking

Browse files
Files changed (12) hide show
  1. .gitattributes +7 -0
  2. .github/workflows/main.yml +47 -0
  3. Dockerfile +17 -0
  4. README.md +9 -0
  5. app.py +360 -0
  6. css/style.css +837 -0
  7. index.html +265 -0
  8. js/markdownRenderer.js +278 -0
  9. js/script.js +1221 -0
  10. logo.png +3 -0
  11. nginx.conf +27 -0
  12. 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,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"
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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}&current=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

  • SHA256: 5b5c61da38a4d6c67a493b31729e3a32395a2b636bc8e316916f873f19ac79d0
  • Pointer size: 131 Bytes
  • Size of remote file: 746 kB
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