// ================================================================================
// CODE VED - MAIN APPLICATION LOGIC (script.js)
// Engineered by Divy Patel | Modular Architecture
// ================================================================================
(function() {
'use strict';
// ----------------------------------------------------------------------------
// 1. CONFIGURATION & GLOBAL STATE
// ----------------------------------------------------------------------------
const Config = {
GAS_URL: "https://script.google.com/macros/s/AKfycbzNO3inVc33ImhfLyde-JjjK9ZlPckLBksqCnCzelfhcklX6mp8KW8vfPTW4oWJTCcN/exec",
API_ENDPOINT: "/api/chat",
LOGO: "logo.png"
};
const State = {
user: localStorage.getItem('codeved_user') || null,
name: localStorage.getItem('codeved_name') || null,
guestCount: (() => {
const v = parseInt(localStorage.getItem('codeved_guest') || '0', 10);
return isNaN(v) ? 0 : v;
})(),
attachment: null,
isProcessing: false,
history: [],
currentThreadId: null,
currentTitle: null,
abortController: null,
location: null,
weatherContext: null,
thinkingMode: false,
thinkingEffort: "medium",
searchEnabled: false,
lastUserMessage: null
};
if (typeof pdfjsLib !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
// ----------------------------------------------------------------------------
// 2. UTILITIES & CUSTOM CONFIRM
// ----------------------------------------------------------------------------
function customConfirm(message) {
return new Promise((resolve) => {
const overlay = document.getElementById('customConfirmOverlay');
const msgEl = document.getElementById('customConfirmMsg');
const cancelBtn = document.getElementById('customConfirmCancel');
const okBtn = document.getElementById('customConfirmOk');
msgEl.textContent = message;
overlay.style.display = 'flex';
function cleanup() {
overlay.style.display = 'none';
cancelBtn.removeEventListener('click', onCancel);
okBtn.removeEventListener('click', onOk);
}
function onCancel() { cleanup(); resolve(false); }
function onOk() { cleanup(); resolve(true); }
cancelBtn.addEventListener('click', onCancel);
okBtn.addEventListener('click', onOk);
});
}
// ----------------------------------------------------------------------------
// 3. UI MANAGER
// ----------------------------------------------------------------------------
const UI = {
autoScroll: true,
toggleSidebar() {
const sb = document.getElementById('sidebar');
const overlay = document.getElementById('mobileOverlay');
sb.classList.toggle('collapsed');
overlay.classList.toggle('active', !sb.classList.contains('collapsed') && window.innerWidth <= 900);
},
toggleAttachMenu() {
document.getElementById('attachMenu').classList.toggle('active');
document.getElementById('thinkMenu').classList.remove('active');
document.getElementById('voiceMenu').classList.remove('active');
},
autoGrow(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 180) + 'px';
},
scrollToBottom(force = false) {
const c = document.getElementById('chatContainer');
if (force || this.autoScroll) {
c.scrollTop = c.scrollHeight;
}
},
escape(s) {
return String(s).replace(/&/g, '&').replace(//g, '>');
},
showAuthMsg(msg, isError = true) {
const el = document.getElementById('authMsg');
el.innerText = msg;
el.style.color = isError ? 'var(--brand-danger)' : 'var(--brand-success)';
setTimeout(() => el.innerText = '', 4000);
},
updateUserInfo(name, email) {
if (name) {
State.name = name;
localStorage.setItem('codeved_name', name);
}
const dispName = State.name || email.split('@')[0];
document.getElementById('uAv').innerText = dispName.charAt(0).toUpperCase();
document.getElementById('uName').innerText = dispName;
document.getElementById('uSub').innerText = email;
document.getElementById('btnLogout').style.display = 'flex';
document.getElementById('btnDeleteAcc').style.display = 'flex';
document.getElementById('btnLoginRegister').style.display = 'none';
document.getElementById('welcomeTitle').innerHTML = `Hello, ${dispName}!
How can I help you today?`;
},
updateWelcomeScreen() {
document.getElementById('welcomeScreen').style.display = State.history.length === 0 ? 'flex' : 'none';
},
initListeners() {
// Sidebar & Overlays
document.getElementById('btnMenuOpen').addEventListener('click', () => this.toggleSidebar());
document.getElementById('btnCloseSidebar').addEventListener('click', () => this.toggleSidebar());
document.getElementById('mobileOverlay').addEventListener('click', () => this.toggleSidebar());
// Scroll to bottom
document.getElementById('scrollToBottomBtn').addEventListener('click', () => {
this.scrollToBottom(true);
this.autoScroll = true;
});
// Input Dock Expanding Logic
const inp = document.getElementById('mainInput');
const dockWrapper = document.getElementById('dockWrapper');
inp.addEventListener('focus', () => dockWrapper.classList.add('expanded'));
inp.addEventListener('blur', () => {
if (inp.value.trim() === '') dockWrapper.classList.remove('expanded');
});
inp.addEventListener('input', () => {
this.autoGrow(inp);
if (inp.value.trim() !== '') dockWrapper.classList.add('expanded');
});
inp.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
Chat.handleSend();
}
});
// Chat Container Scroll Listener
const chatContainerEl = document.getElementById('chatContainer');
const scrollBtnEl = document.getElementById('scrollToBottomBtn');
chatContainerEl.addEventListener('scroll', () => {
const scrollBottom = chatContainerEl.scrollHeight - chatContainerEl.scrollTop - chatContainerEl.clientHeight;
this.autoScroll = scrollBottom <= 50;
scrollBtnEl.classList.toggle('visible', !this.autoScroll);
});
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.tools-left')) {
document.getElementById('attachMenu').classList.remove('active');
document.getElementById('thinkMenu').classList.remove('active');
document.getElementById('voiceMenu').classList.remove('active');
}
});
}
};
// ----------------------------------------------------------------------------
// 4. AUTH MANAGER
// ----------------------------------------------------------------------------
const Auth = {
init() {
if (State.user) {
UI.updateUserInfo(State.name, State.user);
HistoryManager.syncAllChats();
} else {
document.getElementById('uAv').innerText = "G";
document.getElementById('uName').innerText = "Guest Mode";
document.getElementById('uSub').innerText = `Queries: ${State.guestCount}/10`;
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnDeleteAcc').style.display = 'none';
document.getElementById('btnLoginRegister').style.display = 'inline-flex';
document.getElementById('welcomeTitle').innerHTML = `Hello, Guest!
How can I help you today?`;
}
},
async handleLogout() {
const ok = await customConfirm("Are you sure you want to logout?");
if (ok) {
localStorage.removeItem('codeved_user');
localStorage.removeItem('codeved_name');
location.reload();
}
},
async handleDeleteAccount() {
const ok = await customConfirm("Are you sure you want to permanently delete your account and all data?");
if (ok) {
try {
await fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: "delete_account", email: State.user })
});
} catch(e) { console.error("Account delete error:", e); }
finally {
localStorage.removeItem('codeved_user');
localStorage.removeItem('codeved_name');
location.reload();
}
}
},
openModal() { document.getElementById('authModal').style.display = 'flex'; },
closeModal() { document.getElementById('authModal').style.display = 'none'; },
switchTab(tab) {
document.getElementById('tabLogin').classList.remove('active');
document.getElementById('tabRegister').classList.remove('active');
document.getElementById('flowLogin').style.display = 'none';
document.getElementById('flowRegister').style.display = 'none';
if (tab === 'login') {
document.getElementById('tabLogin').classList.add('active');
document.getElementById('flowLogin').style.display = 'block';
} else {
document.getElementById('tabRegister').classList.add('active');
document.getElementById('flowRegister').style.display = 'block';
}
},
switchPhase(from, to) {
document.getElementById(from).classList.remove('active');
document.getElementById(to).classList.add('active');
},
async process(action) {
let payload = { action: action };
let btnId = '';
if (action === 'register_send_otp') {
payload.name = document.getElementById('regName').value.trim();
payload.email = document.getElementById('regEmail').value.trim();
payload.phone = "0000000000";
payload.organization = "CODE VED";
if (!payload.name || !payload.email) return UI.showAuthMsg("Details missing.");
btnId = 'btnRegOtp';
} else if (action === 'login_send_otp') {
payload.email = document.getElementById('logEmail').value.trim();
if (!payload.email) return UI.showAuthMsg("Email required.");
btnId = 'btnLogOtp';
} else if (action === 'register_verify' || action === 'login_verify') {
payload.email = document.getElementById(action === 'register_verify' ? 'regEmail' : 'logEmail').value.trim();
payload.otp = document.getElementById(action === 'register_verify' ? 'regOtp' : 'logOtp').value.trim();
if (!payload.otp) return UI.showAuthMsg("OTP required.");
btnId = action === 'register_verify' ? 'btnRegVerify' : 'btnLogVerify';
}
const btn = document.getElementById(btnId);
const originalText = btn.innerText;
btn.disabled = true;
btn.innerText = "Wait...";
try {
const res = await fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify(payload)
});
const textResponse = await res.text();
let data;
try { data = JSON.parse(textResponse); }
catch (parseErr) {
UI.showAuthMsg("Invalid response from server. Network error.");
btn.disabled = false; btn.innerText = originalText; return;
}
if (data.status === 'success') {
UI.showAuthMsg(data.message, false);
if (action === 'register_send_otp') Auth.switchPhase('regPhase1', 'regPhase2');
else if (action === 'login_send_otp') Auth.switchPhase('loginPhase1', 'loginPhase2');
else {
localStorage.setItem('codeved_user', payload.email);
if (data.user && data.user.name) localStorage.setItem('codeved_name', data.user.name);
location.reload();
}
} else {
UI.showAuthMsg(data.message);
}
} catch (e) {
UI.showAuthMsg("Network Error. Please try again.");
}
btn.disabled = false;
btn.innerText = originalText;
},
initListeners() {
document.getElementById('btnLoginRegister').addEventListener('click', () => this.openModal());
document.getElementById('btnLogout').addEventListener('click', () => this.handleLogout());
document.getElementById('btnDeleteAcc').addEventListener('click', () => this.handleDeleteAccount());
document.getElementById('btnAuthClose').addEventListener('click', () => this.closeModal());
document.getElementById('authModal').addEventListener('click', (e) => {
if (e.target.id === 'authModal') this.closeModal();
});
document.getElementById('tabLogin').addEventListener('click', () => this.switchTab('login'));
document.getElementById('tabRegister').addEventListener('click', () => this.switchTab('register'));
document.getElementById('btnLogOtp').addEventListener('click', () => this.process('login_send_otp'));
document.getElementById('btnLogVerify').addEventListener('click', () => this.process('login_verify'));
document.getElementById('btnRegOtp').addEventListener('click', () => this.process('register_send_otp'));
document.getElementById('btnRegVerify').addEventListener('click', () => this.process('register_verify'));
}
};
// ----------------------------------------------------------------------------
// 5. HISTORY MANAGER
// ----------------------------------------------------------------------------
const HistoryManager = {
async syncAllChats() {
if (!State.user) return;
try {
const res = await fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: "get_all_chats", email: State.user })
});
const data = JSON.parse(await res.text());
if (data.status === 'success') {
if (data.user) UI.updateUserInfo(data.user.name, data.user.email);
this.renderSidebar(data.chats || []);
}
} catch (e) { console.error("History sync error:", e); }
},
renderSidebar(chats) {
const container = document.getElementById('chatHistory');
container.innerHTML = `
`;
document.getElementById('btnClearAll').addEventListener('click', () => this.clearAll());
chats.forEach(chat => {
const isActive = State.currentThreadId === chat.threadId ? 'active' : '';
const item = document.createElement('div');
item.className = `history-item ${isActive}`;
item.dataset.threadId = chat.threadId;
item.innerHTML = `
${UI.escape(chat.title)}
`;
container.appendChild(item);
});
},
async loadChat(threadId) {
if (!State.user || State.isProcessing) return;
State.currentThreadId = threadId;
document.getElementById('chatMessages').innerHTML = '';
try {
const res = await fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: "get_chat", email: State.user, threadId: threadId })
});
const data = JSON.parse(await res.text());
if (data.status === 'success') {
State.history = JSON.parse(data.historyJSON || "[]");
UI.updateWelcomeScreen();
for (const msg of State.history) {
if (msg.role === 'user') {
let dispText = msg.content;
if (dispText.includes('[SYSTEM REAL-TIME WEATHER:')) dispText = dispText.split('\n\n[SYSTEM REAL-TIME WEATHER:')[0];
if (dispText.includes('---END DATA---')) {
const afterMarker = dispText.split('---END DATA---')[1] || '';
dispText = afterMarker.replace(/^\s*User:\s*/, '').trim() || '[Attached File]';
}
Chat.renderUser(dispText);
} else {
const msgId = 'hist_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
const botObj = Chat.renderBot(msgId);
Chat.parseAndRender(msg.content, false, botObj.contentDiv);
TTSManager.autoPrepare(msgId, msg.content, botObj.listenBtn);
}
}
UI.autoScroll = true;
UI.scrollToBottom();
if (window.innerWidth <= 900) UI.toggleSidebar();
}
} catch (e) { console.error("Load chat error:", e); }
},
startNew() {
if (State.isProcessing) return;
State.currentThreadId = null;
State.currentTitle = null;
State.history = [];
document.getElementById('chatMessages').innerHTML = '';
UI.updateWelcomeScreen();
UI.autoScroll = true;
UI.scrollToBottom();
if (window.innerWidth <= 900) UI.toggleSidebar();
},
saveCurrent() {
if (!State.user || State.history.length === 0) return;
if (!State.currentThreadId) {
State.currentThreadId = "thr_" + Date.now();
const firstUser = State.history.find(m => m.role === 'user');
State.currentTitle = firstUser ? firstUser.content.substring(0, 25) : "New Workspace";
}
fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({
action: "save_chat",
email: State.user,
threadId: State.currentThreadId,
title: State.currentTitle,
historyJSON: JSON.stringify(State.history)
})
}).catch(e => console.error("Save error:", e)).then(() => this.syncAllChats());
},
async clearAll() {
const ok = await customConfirm("Clear all workspaces?");
if (ok) {
State.history = [];
State.currentThreadId = null;
State.currentTitle = null;
document.getElementById('chatMessages').innerHTML = '';
UI.updateWelcomeScreen();
UI.autoScroll = true;
UI.scrollToBottom();
fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: "clear_all_chats", email: State.user })
}).catch(e => console.error("Clear error:", e)).then(() => this.syncAllChats());
}
},
async deleteChat(threadId) {
const ok = await customConfirm("Delete this workspace?");
if (ok) {
if (State.currentThreadId === threadId) this.startNew();
fetch(Config.GAS_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({ action: "delete_chat", email: State.user, threadId: threadId })
}).catch(e => console.error("Delete error:", e)).then(() => this.syncAllChats());
}
},
initListeners() {
document.getElementById('btnNewChat').addEventListener('click', () => this.startNew());
// Event Delegation for History List
document.getElementById('chatHistory').addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.btn-delete-chat');
if (deleteBtn) {
e.stopPropagation();
this.deleteChat(deleteBtn.dataset.deleteId);
return;
}
const historyItem = e.target.closest('.history-item');
if (historyItem && historyItem.dataset.threadId) {
this.loadChat(historyItem.dataset.threadId);
}
});
}
};
// ----------------------------------------------------------------------------
// 6. ENVIRONMENT, SEARCH & THINKING MANAGERS
// ----------------------------------------------------------------------------
const EnvironmentManager = {
toggle() {
const status = document.getElementById('locStatus');
document.getElementById('attachMenu').classList.remove('active');
if (State.location) {
State.location = null;
State.weatherContext = null;
status.style.display = 'none';
} else {
if (navigator.geolocation) {
status.style.display = 'flex';
status.innerHTML = `Finding location...`;
navigator.geolocation.getCurrentPosition(pos => {
State.location = { lat: pos.coords.latitude, lng: pos.coords.longitude };
status.innerHTML = `✓ Location active!`;
setTimeout(() => status.style.display = 'none', 3000);
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${pos.coords.latitude}&longitude=${pos.coords.longitude}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m`)
.then(r => r.ok ? r.json() : null)
.then(data => {
const c = data && data.current;
if (c) State.weatherContext = `Temperature: ${c.temperature_2m}°C, Humidity: ${c.relative_humidity_2m}%, Wind: ${c.wind_speed_10m} km/h`;
}).catch(() => {});
}, err => {
status.innerHTML = `Location denied.`;
setTimeout(() => status.style.display = 'none', 3000);
});
}
}
}
};
const SearchManager = {
toggle() {
State.searchEnabled = !State.searchEnabled;
const btn = document.getElementById('btnSearch');
btn.classList.toggle('active-search', State.searchEnabled);
btn.title = State.searchEnabled ? 'Web Search: ON' : 'Web Search: OFF';
}
};
const ThinkingManager = {
toggleMenu() {
document.getElementById('thinkMenu').classList.toggle('active');
document.getElementById('attachMenu').classList.remove('active');
},
setEffort(level) {
State.thinkingMode = true;
State.thinkingEffort = level;
document.getElementById('thinkMenu').classList.remove('active');
this.updateIndicator();
},
disable() {
State.thinkingMode = false;
document.getElementById('thinkMenu').classList.remove('active');
this.updateIndicator();
},
updateIndicator() {
const btn = document.getElementById('btnAttach');
btn.classList.remove('active-think-low', 'active-think-medium', 'active-think-high');
if (State.thinkingMode) {
btn.classList.add(`active-think-${State.thinkingEffort}`);
btn.title = `Thinking Mode: ${State.thinkingEffort.charAt(0).toUpperCase() + State.thinkingEffort.slice(1)}`;
} else {
btn.title = 'Attach File';
}
},
initListeners() {
document.getElementById('thinkMenu').addEventListener('click', (e) => {
const item = e.target.closest('.dropdown-item');
if (!item) return;
if (item.id === 'optDisableThink') {
this.disable();
} else if (item.dataset.effort) {
this.setEffort(item.dataset.effort);
}
});
}
};
// ----------------------------------------------------------------------------
// 7. FILE SYSTEM & SPEECH
// ----------------------------------------------------------------------------
const FileSys = {
async process(input, type) {
document.getElementById('attachMenu').classList.remove('active');
const file = input.files[0];
if (!file) return;
document.getElementById('filePreviewBar').classList.add('active');
document.getElementById('fileName').innerText = file.name;
if (type === 'image') {
document.getElementById('imgPreview').style.display = 'block';
document.getElementById('docPreview').style.display = 'none';
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
let w = img.width, h = img.height;
const max = 1024;
if(w > max || h > max) {
if(w > h) { h = (max/w)*h; w = max; }
else { w = (max/h)*w; h = max; }
}
canvas.width = w; canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const b64 = canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
State.attachment = { type: 'image', data: b64, name: file.name };
document.getElementById('imgPreview').src = `data:image/jpeg;base64,${b64}`;
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
} else {
document.getElementById('imgPreview').style.display = 'none';
document.getElementById('docPreview').style.display = 'block';
if (file.name.endsWith('.pdf')) {
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let text = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
text += content.items.map(item => item.str).join(' ') + '\n';
}
State.attachment = { type: 'text', data: text, name: file.name };
} catch(e) { State.attachment = { type: 'text', data: '[PDF parsing error]', name: file.name }; }
} else if (file.name.endsWith('.docx')) {
try {
const arrayBuffer = await file.arrayBuffer();
const result = await mammoth.extractRawText({ arrayBuffer });
State.attachment = { type: 'text', data: result.value, name: file.name };
} catch(e) { State.attachment = { type: 'text', data: '[DOCX parsing error]', name: file.name }; }
} else {
const reader = new FileReader();
reader.onload = (e) => { State.attachment = { type: 'text', data: e.target.result, name: file.name }; };
reader.readAsText(file);
}
}
input.value = '';
document.getElementById('dockWrapper').classList.add('expanded');
},
discard() {
State.attachment = null;
document.getElementById('filePreviewBar').classList.remove('active');
if (document.getElementById('mainInput').value.trim() === '') {
document.getElementById('dockWrapper').classList.remove('expanded');
}
},
initListeners() {
document.getElementById('btnRemoveFile').addEventListener('click', () => this.discard());
document.getElementById('imgUpload').addEventListener('change', (e) => this.process(e.target, 'image'));
document.getElementById('docUpload').addEventListener('change', (e) => this.process(e.target, 'document'));
}
};
const Speech = {
rec: null,
isRec: false,
init() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return;
this.rec = new SR();
this.rec.continuous = false;
this.rec.interimResults = true;
this.rec.lang = navigator.language || 'en-US';
this.rec.onstart = () => {
this.isRec = true;
document.getElementById('btnStt').classList.add('recording');
};
this.rec.onresult = (e) => {
if (!e.results || !e.results.length) return;
let trans = '';
for (let i = e.resultIndex; i < e.results.length; ++i) {
if (e.results[i].isFinal && e.results[i][0]) trans += e.results[i][0].transcript;
}
if (trans) {
const input = document.getElementById('mainInput');
input.value += (input.value ? ' ' : '') + trans;
UI.autoGrow(input);
document.getElementById('dockWrapper').classList.add('expanded');
}
};
this.rec.onerror = () => this.stop();
this.rec.onend = () => this.stop();
},
toggle() {
if (!this.rec) this.init();
if (this.isRec) this.stop();
else { try { this.rec.start(); } catch(e) {} }
},
stop() {
if(this.rec) this.rec.stop();
this.isRec = false;
document.getElementById('btnStt').classList.remove('recording');
},
initListeners() {
document.getElementById('btnStt').addEventListener('click', () => this.toggle());
}
};
// ----------------------------------------------------------------------------
// 8. TTS MANAGER
// ----------------------------------------------------------------------------
const TTSManager = {
cache: {},
currentAudio: null,
selectedVoice: "M2",
selectedLanguage: "English",
isPlaying: false,
currentMsgId: null,
currentBtnElement: null,
preparingSet: new Set(),
voiceMap: {
"M1": "Aarav", "M2": "Kabir", "M3": "Vihaan", "M4": "Advik", "M5": "Rohan",
"F1": "Priya", "F2": "Ananya", "F3": "Diya", "F4": "Sneha", "F5": "Kavya"
},
languageMap: {
"English": "en", "Korean": "ko", "Japanese": "ja", "Arabic": "ar", "Bulgarian": "bg",
"Czech": "cs", "Danish": "da", "German": "de", "Greek": "el", "Spanish": "es",
"Estonian": "et", "Finnish": "fi", "French": "fr", "Hindi": "hi", "Croatian": "hr",
"Hungarian": "hu", "Indonesian": "id", "Italian": "it", "Lithuanian": "lt", "Latvian": "lv",
"Dutch": "nl", "Polish": "pl", "Portuguese": "pt", "Romanian": "ro", "Russian": "ru",
"Slovak": "sk", "Slovenian": "sl", "Swedish": "sv", "Turkish": "tr", "Ukrainian": "uk",
"Vietnamese": "vi"
},
initUI() {
const list = document.getElementById('voiceOptionsList');
list.innerHTML = '';
for (const [code, name] of Object.entries(this.voiceMap)) {
const isSelected = code === this.selectedVoice;
const checkIcon = isSelected ? `` : ``;
const item = document.createElement('div');
item.className = `dropdown-item ${isSelected ? 'selected' : ''}`;
item.dataset.voiceCode = code;
item.innerHTML = `${name}${checkIcon}`;
list.appendChild(item);
}
const langList = document.getElementById('languageOptionsList');
langList.innerHTML = '';
for (const [name, code] of Object.entries(this.languageMap)) {
const isSelected = name === this.selectedLanguage;
const checkIcon = isSelected ? `` : ``;
const item = document.createElement('div');
item.className = `dropdown-item ${isSelected ? 'selected' : ''}`;
item.dataset.langName = name;
item.innerHTML = `${name}${checkIcon}`;
langList.appendChild(item);
}
},
setVoice(code) {
Object.values(this.cache).forEach(url => URL.revokeObjectURL(url));
this.cache = {};
this.selectedVoice = code;
this.initUI();
document.getElementById('voiceMenu').classList.remove('active');
},
setLanguage(name) {
Object.values(this.cache).forEach(url => URL.revokeObjectURL(url));
this.cache = {};
this.selectedLanguage = name;
this.initUI();
document.getElementById('voiceMenu').classList.remove('active');
},
cleanTextForTTS(text) {
let cleaned = text.replace(/\u0060{3}[\s\S]*?\u0060{3}/g, '')
.replace(/\u0060[^\u0060]*\u0060/g, '')
.replace(/[\s\S]*?<\/think>/gi, '')
.replace(/https?:\/\/[^\s]+/g, '')
.replace(/[*_#\u0060~>]/g, '')
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim();
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}]/gu, '')
.replace(/[\u{1F300}-\u{1F5FF}]/gu, '')
.replace(/[\u{1F680}-\u{1F6FF}]/gu, '')
.replace(/[\u{2600}-\u{26FF}]/gu, '')
.replace(/[\u{2700}-\u{27BF}]/gu, '');
if (cleaned.length > 1500) cleaned = cleaned.substring(0, 1500) + '...';
return cleaned;
},
async autoPrepare(msgId, fullText, btnElement) {
if (this.preparingSet.has(msgId)) return;
const cleanText = this.cleanTextForTTS(fullText);
if (!cleanText) { btnElement.style.display = 'none'; return; }
this.preparingSet.add(msgId);
btnElement.style.display = 'flex';
btnElement.innerHTML = ` Preparing...`;
btnElement.style.pointerEvents = 'none';
try {
const response = await fetch('/api/tts', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ text: cleanText, voice: this.selectedVoice, language_name: this.selectedLanguage })
});
if (!response.ok) throw new Error('TTS service unavailable');
const blob = await response.blob();
this.cache[msgId] = URL.createObjectURL(blob);
btnElement.innerHTML = ` Listen`;
btnElement.style.pointerEvents = 'auto';
} catch(e) {
btnElement.innerHTML = `⚠️ ${e.message}`;
setTimeout(() => { btnElement.style.display = 'none'; }, 4000);
} finally { this.preparingSet.delete(msgId); }
},
play(msgId, btnElement) {
const audioUrl = this.cache[msgId];
if (!audioUrl) {
this.autoPrepare(msgId, document.getElementById(`bot-content-${msgId}`).innerText, btnElement);
return;
}
if (this.isPlaying && this.currentMsgId === msgId) {
if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; }
this.isPlaying = false;
this.currentMsgId = null;
btnElement.innerHTML = ` Listen`;
btnElement.style.background = '';
return;
}
if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; }
this.isPlaying = true;
this.currentMsgId = msgId;
this.currentBtnElement = btnElement;
const originalHTML = btnElement.innerHTML;
btnElement.innerHTML = ` Playing...`;
btnElement.style.background = 'rgba(211, 227, 253, 0.9)';
this.currentAudio = new Audio(audioUrl);
this.currentAudio.playbackRate = 1.15;
const resetBtn = () => {
this.isPlaying = false;
this.currentMsgId = null;
btnElement.innerHTML = originalHTML;
btnElement.style.background = '';
};
this.currentAudio.onended = resetBtn;
this.currentAudio.onerror = resetBtn;
this.currentAudio.play();
},
initListeners() {
document.getElementById('voiceMenu').addEventListener('click', (e) => {
const item = e.target.closest('.dropdown-item');
if (!item) return;
if (item.dataset.voiceCode) this.setVoice(item.dataset.voiceCode);
else if (item.dataset.langName) this.setLanguage(item.dataset.langName);
});
}
};
// ----------------------------------------------------------------------------
// 9. CHAT MANAGER
// ----------------------------------------------------------------------------
const Chat = {
renderUser(txt, attachUI = '') {
const c = document.getElementById('chatMessages');
const w = document.createElement('div');
w.className = 'message-wrapper';
w.innerHTML = `${attachUI}${txt ? UI.escape(txt) : ''}
`;
c.appendChild(w);
UI.autoScroll = true;
UI.scrollToBottom(true);
},
renderBot(msgId) {
const c = document.getElementById('chatMessages');
const w = document.createElement('div');
w.className = 'message-wrapper';
const listenIcon = ``;
const copyIcon = ``;
w.innerHTML = `
`;
c.appendChild(w);
UI.scrollToBottom();
return {
contentDiv: document.getElementById(`bot-content-${msgId}`),
listenBtn: document.getElementById(`listen-btn-${msgId}`),
wrapper: w,
msgId: msgId
};
},
// Delegating Markdown Rendering to the dedicated module
parseAndRender(fullText, isProcessing, container) {
if (window.MarkdownRenderer && typeof window.MarkdownRenderer.parseAndRender === 'function') {
window.MarkdownRenderer.parseAndRender(fullText, isProcessing, container);
} else {
// Fallback if markdownRenderer.js hasn't loaded yet
container.innerHTML = fullText;
}
},
copyMsg(contentDiv) {
const clone = contentDiv.cloneNode(true);
const thinkBox = clone.querySelector('.qwen-think-box');
if (thinkBox) thinkBox.remove();
navigator.clipboard.writeText(clone.innerText).then(() => {
const btn = contentDiv.parentElement.querySelector('.copy-btn');
const original = btn.innerHTML;
btn.innerHTML = ` Copied!`;
setTimeout(() => btn.innerHTML = original, 1500);
});
},
stopGeneration() {
if (State.abortController) {
State.abortController.abort();
State.abortController = null;
}
State.isProcessing = false;
document.getElementById('btnSend').disabled = false;
document.getElementById('btnSend').style.display = 'flex';
document.getElementById('btnStop').classList.remove('active');
if (document.getElementById('mainInput').value.trim() === '') {
document.getElementById('dockWrapper').classList.remove('expanded');
}
},
async handleSend() {
if(State.isProcessing) return;
if (!State.user && State.guestCount >= 10) {
Auth.openModal();
UI.showAuthMsg("Guest limit reached (10/10). Please login or register to continue.", true);
return;
}
const input = document.getElementById('mainInput');
const text = input.value.trim();
if(!text && !State.attachment) return;
if (!State.user) {
let count = parseInt(localStorage.getItem('codeved_guest') || '0', 10);
if (isNaN(count)) count = 0;
count++;
State.guestCount = count;
localStorage.setItem('codeved_guest', count.toString());
document.getElementById('uSub').innerText = `Queries: ${count}/10`;
}
State.isProcessing = true;
document.getElementById('btnSend').style.display = 'none';
document.getElementById('btnStop').classList.add('active');
State.lastUserMessage = text;
input.value = '';
input.style.height = 'auto';
UI.updateWelcomeScreen();
let payloadStr = text, attachUI = '', mediaArray = [];
if(State.attachment) {
if(State.attachment.type === 'text') {
payloadStr = `[File attached: ${State.attachment.name}]\n\n---DATA---\n${State.attachment.data}\n---END DATA---\n\nUser: ${text}`;
attachUI = ``;
} else {
mediaArray.push(State.attachment);
attachUI = ``;
}
}
let systemInjectedPayload = payloadStr;
if(State.weatherContext) {
systemInjectedPayload = `${payloadStr}\n\n[SYSTEM REAL-TIME WEATHER: ${State.weatherContext}]`;
}
Chat.renderUser(text, attachUI);
State.history.push({ role: 'user', content: payloadStr });
FileSys.discard();
const msgId = Date.now().toString();
const botObj = Chat.renderBot(msgId);
Chat.parseAndRender("", true, botObj.contentDiv);
State.abortController = new AbortController();
let fullText = "";
try {
const res = await fetch(Config.API_ENDPOINT, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
message: systemInjectedPayload,
attachments: mediaArray,
is_search: State.searchEnabled,
location: State.location,
thinking_mode: State.thinkingMode,
thinking_effort: State.thinkingEffort,
history: State.history
}),
signal: State.abortController.signal
});
if(!res.ok) {
let errMsg = `Server error (${res.status})`;
if (res.status === 404) errMsg = 'Chat API endpoint not found (404).';
else if (res.status === 500) errMsg = 'Internal server error (500).';
throw new Error(errMsg);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while(true) {
const {done, value} = await reader.read();
if(done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop();
for(let line of lines) {
if(line.startsWith('data: ')) {
const dataStr = line.substring(6).trim();
if(dataStr === '[DONE]') continue;
try {
const json = JSON.parse(dataStr);
if (json.error) {
botObj.contentDiv.innerHTML += `
⚠️ ${UI.escape(json.error)}`;
return;
}
if(json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content) {
fullText += json.choices[0].delta.content;
}
} catch(e) {}
Chat.parseAndRender(fullText, true, botObj.contentDiv);
}
}
}
Chat.parseAndRender(fullText, false, botObj.contentDiv);
State.history.push({ role: 'assistant', content: fullText });
HistoryManager.saveCurrent();
} catch(e) {
if (e.name === 'AbortError') {
botObj.contentDiv.innerHTML += `
⏹ Generation stopped.`;
if (fullText.trim()) {
State.history.push({ role: 'assistant', content: fullText });
HistoryManager.saveCurrent();
}
} else {
botObj.contentDiv.innerHTML += `
⚠️ ${UI.escape(e.message || 'Connection Offline.')}`;
}
} finally {
State.isProcessing = false;
document.getElementById('btnSend').disabled = false;
document.getElementById('btnSend').style.display = 'flex';
document.getElementById('btnStop').classList.remove('active');
if (document.getElementById('mainInput').value.trim() === '') {
document.getElementById('dockWrapper').classList.remove('expanded');
}
if (UI.autoScroll) UI.scrollToBottom();
if(botObj.listenBtn && fullText.trim().length > 0) {
TTSManager.autoPrepare(msgId, fullText, botObj.listenBtn);
}
}
},
initListeners() {
document.getElementById('btnSend').addEventListener('click', () => this.handleSend());
document.getElementById('btnStop').addEventListener('click', () => this.stopGeneration());
// Event Delegation for Chat Messages (Copy & Listen buttons)
document.getElementById('chatMessages').addEventListener('click', (e) => {
const listenBtn = e.target.closest('.listen-btn');
if (listenBtn) {
const msgId = listenBtn.id.replace('listen-btn-', '');
TTSManager.play(msgId, listenBtn);
return;
}
const copyBtn = e.target.closest('.copy-btn');
if (copyBtn) {
const contentDiv = copyBtn.closest('.bot-content-wrapper').querySelector('.bot-content');
this.copyMsg(contentDiv);
return;
}
// Code block copy delegation (handled by markdownRenderer usually, but safe fallback here)
const codeCopyBtn = e.target.closest('.code-copy-btn');
if (codeCopyBtn && !codeCopyBtn.classList.contains('copied')) {
const pre = codeCopyBtn.closest('pre');
const codeEl = pre.querySelector('code');
if(codeEl) {
navigator.clipboard.writeText(codeEl.innerText).then(() => {
codeCopyBtn.classList.add('copied');
codeCopyBtn.innerHTML = `Copied!`;
setTimeout(() => {
codeCopyBtn.classList.remove('copied');
codeCopyBtn.innerHTML = `Copy`;
}, 1500);
});
}
}
});
}
};
// ----------------------------------------------------------------------------
// 10. ATTACH MENU & INITIALIZATION
// ----------------------------------------------------------------------------
function initAttachMenuListeners() {
document.getElementById('btnAttach').addEventListener('click', () => UI.toggleAttachMenu());
document.getElementById('optUploadImg').addEventListener('click', () => document.getElementById('imgUpload').click());
document.getElementById('optUploadDoc').addEventListener('click', () => document.getElementById('docUpload').click());
document.getElementById('optEnv').addEventListener('click', () => EnvironmentManager.toggle());
document.getElementById('optThink').addEventListener('click', () => ThinkingManager.toggleMenu());
document.getElementById('optVoice').addEventListener('click', () => TTSManager.toggleMenu());
document.getElementById('btnSearch').addEventListener('click', () => SearchManager.toggle());
}
function initGlobal() {
// Set Logos
document.getElementById('faviconLink').href = Config.LOGO;
document.getElementById('welcomeLogoImg').src = Config.LOGO;
document.getElementById('sidebarLogoImg').src = Config.LOGO;
// Initialize all listeners
UI.initListeners();
Auth.initListeners();
HistoryManager.initListeners();
ThinkingManager.initListeners();
FileSys.initListeners();
Speech.initListeners();
TTSManager.initListeners();
Chat.initListeners();
initAttachMenuListeners();
// Initial State Setup
Auth.init();
TTSManager.initUI();
UI.updateWelcomeScreen();
if(window.innerWidth > 900) {
document.getElementById('sidebar').classList.remove('collapsed');
}
}
// Run when DOM is fully ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGlobal);
} else {
initGlobal();
}
})();