window.onerror = (msg, url, line) => {
const banner = document.getElementById('debug-banner');
if (banner) {
banner.textContent = `JS Error: ${msg} (line ${line})`;
banner.classList.remove('hidden');
}
};
// ============================================================
// CONFIG & STATE
// ============================================================
const STREAM_API_URL = "https://aqhedgyiworhbfkrzreb.supabase.co/functions/v1/quick-processor";
const TTS_API_URL = "https://aqhedgyiworhbfkrzreb.supabase.co/functions/v1/swift-worker";
const TRANSLITERATE_API_URL = "https://aqhedgyiworhbfkrzreb.supabase.co/functions/v1/quick-task";
const RAZORPAY_KEY_ID = "rzp_test_TZ9SeSSO5G8eCq";
const firebaseConfig = {
apiKey: "AIzaSyAChu97QI-tyulpisxH6aECBYlj88DGB1o",
authDomain: "arki-ai-280b8.firebaseapp.com",
projectId: "arki-ai-280b8",
storageBucket: "arki-ai-280b8.firebasestorage.app",
messagingSenderId: "1075792140029",
appId: "1:1075792140029:web:9f3af7c6b89530c7ee9094",
measurementId: "G-PL1YY34MBZ"
};
const UI_TRANSLATIONS = {
en: {
welcomeTitle: "Welcome",
welcomeSub: "Start chatting with Orki AI",
inputPlaceholder: "Message Orki...",
newChat: "New Chat",
conversations: "Conversations",
dailyQuota: "Daily Quota",
disclaimer: "Orki AI can make mistakes. Always double check."
},
deva: {
welcomeTitle: "बरायबाय",
welcomeSub: "Orki AI जों सावरायनायखौ जागायदो",
inputPlaceholder: "Orki नो लिरहर...",
newChat: "गोदानै सावराय",
conversations: "सावरायनायफोर",
dailyQuota: "सानसेनि बाहायनाय",
disclaimer: "Orki AI आबो गोरोन्थि खालामनो हागौ। खेबसे नायबिजिर फिन।"
},
roman: {
welcomeTitle: "Boraibai",
welcomeSub: "Orki AI jwng saorainaikow jagaidw",
inputPlaceholder: "Orki nw lirhor...",
newChat: "Gwdanwi Saorai",
conversations: "Saorainaifwr",
dailyQuota: "Sanseni Bahainai",
disclaimer: "Orki AI bw gwrwnti khalamnw hagwu. kebse nai bijir fin."
}
};
const DAILY_LIMITS = { Guest: 5, Free: 20, Plus: 60, Pro: Infinity };
const MEMORY_DEPTH = { Guest: -4, Free: -6, Plus: -12, Pro: -60 };
const PRICING = {
weekly: { plus: 29, pro: 59, plusPaise: 2900, proPaise: 5900, label: '/wk' },
monthly: { plus: 99, pro: 199, plusPaise: 9900, proPaise: 19900, label: '/mo' },
yearly: { plus: 799, pro: 1599, plusPaise: 79900, proPaise: 159900, label: '/yr' }
};
const $ = id => document.getElementById(id);
const on = (id, event, handler) => {
const el = $(id);
if (el) el.addEventListener(event, handler);
};
const safeGet = (key, fallback) => {
try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
};
const safeSet = (key, val) => {
try { localStorage.setItem(key, val); } catch {}
};
const getTodayString = () => new Date().toISOString().split('T')[0];
let currentScript = safeGet('arki_script', 'deva');
let uiLanguage = safeGet('arki_ui_lang', 'deva');
let userPersona = safeGet('arki_persona', '');
let userName = safeGet('arki_username', '');
let currentUser = null, currentPlan = "Free", selectedModel = 'orki-3.0', currentDailyUsage = 0;
let abortController = null, isGenerating = false, currentChatId = null;
let currentChatMessages = [], selectedCycle = 'monthly', cachedThreads = [], isIncognito = false;
let mediaRecorder = null, audioChunks = [], isRecording = false;
let isLiveTalkActive = false, liveTalkAudioCtx = null, liveTalkAnalyser = null, liveTalkSilenceTimer = null, liveTalkStream = null;
let currentAudio = null, activeTtsBtn = null, ttsAudioCache = new Map();
// Unlock mobile autoplay by priming a silent clip on a real user tap.
// Reuses the same currentAudio instance that playBodoSpeech/bindAudioToPlayer
// already control, so there's only ever one audio player in the app.
function unlockAudio() {
if (!currentAudio) currentAudio = new Audio();
currentAudio.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA';
currentAudio.play().catch(() => {});
}
// Synthesized "live talk opening" chime — a soft rising two-note
// swell, generated in-browser so it plays instantly with zero
// network requests or bundled audio files.
function playLiveTalkOpenSound() {
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioCtx();
const now = ctx.currentTime;
const notes = [
{ freq: 523.25, start: 0, dur: 0.16 }, // C5
{ freq: 783.99, start: 0.10, dur: 0.22 } // G5
];
notes.forEach(({ freq, start, dur }) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + start);
gain.gain.setValueAtTime(0, now + start);
gain.gain.linearRampToValueAtTime(0.18, now + start + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + start + dur);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now + start);
osc.stop(now + start + dur + 0.02);
});
setTimeout(() => ctx.close().catch(() => {}), 600);
} catch (e) {
console.warn("Live talk chime unavailable:", e);
}
}
let auth = null, db = null;
// ============================================================
// TRANSLITERATION ENGINE
// ============================================================
async function cleanBodoText(devanagariText) {
try {
const res = await fetch(TRANSLITERATE_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: devanagariText })
});
const data = await res.json();
return data.roman || devanagariText;
} catch (err) {
console.error("Transliteration failed:", err);
return devanagariText;
}
}
// ============================================================
// DOM ELEMENTS
// ============================================================
const chat = $('chat'), form = $('chat-form'), input = $('prompt'), sendBtn = $('send-btn'), thinking = $('thinking'), micBtn = $('mic-btn');
const liveTalkBtn = $('live-talk-btn'), liveTalkPulse = $('live-talk-pulse'), liveTalkIcon = $('live-talk-icon');
const liveTalkOverlay = $('live-talk-overlay'), liveTalkOrb = $('live-talk-orb'), liveTalkRing = $('live-talk-ring');
const liveTalkStatusPill = $('live-talk-status-pill'), liveTalkTranscript = $('live-talk-transcript'), liveTalkEndBtn = $('live-talk-end-btn');
const menuBtn = $('menu-btn'), drawer = $('drawer'), drawerBackdrop = $('drawer-backdrop'), closeDrawer = $('close-drawer');
const newChatBtn = $('new-chat-btn'), incognitoBtn = $('incognito-btn'), incognitoBadge = $('incognito-badge'), clearAllBtn = $('clear-all-btn'), historyList = $('history-list');
const authActionBtn = $('auth-action-btn');
const authModal = $('auth-modal'), authError = $('auth-error');
const userDisplay = $('user-display'), userPlan = $('user-plan'), upgradeBtn = $('upgrade-btn'), syncStatus = $('sync-status');
const modelSelectBtn = $('model-select-btn'), modelDropdown = $('model-dropdown'), currentModelDisplay = $('current-model-display'), selectOrki = $('select-orki'), selectOkafwr = $('select-okafwr'), okafwrLockIcon = $('okafwr-lock-icon');
const usageCountText = $('usage-count-text'), usageProgressBar = $('usage-progress-bar');
const upgradeModal = $('upgrade-modal'), closeUpgrade = $('close-upgrade'), cycleWeekly = $('cycle-weekly'), cycleMonthly = $('cycle-monthly'), cycleYearly = $('cycle-yearly');
const plusPriceText = $('plus-price'), proPriceText = $('pro-price'), buyPlusBtn = $('buy-plus-btn'), buyProBtn = $('buy-pro-btn');
const openSettingsBtn = $('open-settings'), closeSettingsBtn = $('close-settings'), settingsModal = $('settings-modal');
const optDeva = $('opt-deva'), optRom = $('opt-rom'), optUiDeva = $('opt-ui-deva'), optUiRom = $('opt-ui-rom'), optUiEn = $('opt-ui-en');
const usernameInput = $('username-input'), personaInput = $('persona-input'), saveSettingsBtn = $('save-settings');
const ACTIVE_BTN = "py-2 rounded-lg text-xs font-semibold bg-emerald-600 text-white transition cursor-pointer";
const INACTIVE_BTN = "py-2 rounded-lg text-xs font-medium text-zinc-400 hover:text-white transition cursor-pointer";
const stopIcon = ``;
const sendIcon = ``;
const trashIcon = ``;
// ============================================================
// UI HELPERS
// ============================================================
function openAuthModal() {
if (authError) authError.classList.add('hidden');
if (authModal) authModal.classList.remove('hidden');
}
function applyUILanguage(lang) {
const t = UI_TRANSLATIONS[lang] || UI_TRANSLATIONS.en;
const set = (id, val) => { const el = $(id); if (el) el.textContent = val; };
const qSet = (sel, val) => { const el = document.querySelector(sel); if (el) el.textContent = val; };
qSet('#welcome-screen h2', t.welcomeTitle);
qSet('#welcome-screen p', t.welcomeSub);
if (input && currentUser) input.placeholder = t.inputPlaceholder;
set('new-chat-label', t.newChat);
set('quota-label', t.dailyQuota);
set('conversations-label', t.conversations);
set('disclaimer-label', t.disclaimer);
}
function setGeneratingState(generating) {
isGenerating = generating;
if (!sendBtn) return;
if (generating) {
sendBtn.classList.replace('bg-emerald-600', 'bg-red-600');
sendBtn.classList.replace('hover:bg-emerald-500', 'hover:bg-red-500');
sendBtn.title = "Stop generating";
sendBtn.innerHTML = stopIcon;
} else {
sendBtn.classList.replace('bg-red-600', 'bg-emerald-600');
sendBtn.classList.replace('hover:bg-red-500', 'hover:bg-emerald-500');
sendBtn.title = "Send message";
sendBtn.innerHTML = sendIcon;
}
}
function updateUsageUI() {
if (!usageCountText || !usageProgressBar) return;
const limit = DAILY_LIMITS[currentPlan] || 20;
if (limit === Infinity) {
usageCountText.textContent = "Unlimited";
usageCountText.className = "text-[10px] font-bold text-amber-400";
usageProgressBar.style.width = "100%";
usageProgressBar.className = "h-full bg-amber-400 rounded-full";
return;
}
const pct = Math.min(100, Math.round((currentDailyUsage / limit) * 100));
usageCountText.textContent = `${currentDailyUsage} / ${limit}`;
usageCountText.className = pct >= 100 ? "text-[10px] font-bold text-red-400" : "text-[10px] font-medium text-zinc-400";
usageProgressBar.style.width = `${pct}%`;
usageProgressBar.className = `h-full rounded-full transition-all duration-300 ${pct >= 90 ? 'bg-red-500' : pct >= 70 ? 'bg-amber-500' : 'bg-emerald-500'}`;
}
function updateModelLockUI() {
if (!okafwrLockIcon) return;
okafwrLockIcon.textContent = currentPlan === 'Pro' ? '✓' : '🔒';
okafwrLockIcon.className = currentPlan === 'Pro' ? 'text-amber-400 text-xs font-bold' : 'text-zinc-500 text-xs';
}
function updateIncognitoUI() {
if (incognitoBadge) incognitoBadge.classList.toggle('hidden', !isIncognito);
if (incognitoBtn) {
incognitoBtn.className = isIncognito
? "px-3 py-2 rounded-xl bg-purple-950/60 border border-purple-500 text-purple-300 text-xs font-medium transition cursor-pointer flex items-center gap-1 shrink-0 shadow-sm"
: "px-3 py-2 rounded-xl bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-purple-300 text-xs font-medium transition cursor-pointer border border-zinc-700/50 flex items-center gap-1 shrink-0";
}
}
function updatePricingUI() {
const p = PRICING[selectedCycle];
if (plusPriceText) plusPriceText.innerHTML = `₹${p.plus}${p.label}`;
if (proPriceText) proPriceText.innerHTML = `₹${p.pro}${p.label}`;
[cycleWeekly, cycleMonthly, cycleYearly].forEach(btn => {
if (btn) {
const isActive = selectedCycle === btn.id.replace('cycle-', '');
btn.className = `py-1.5 rounded-lg transition cursor-pointer ${isActive ? 'bg-emerald-600 text-white font-semibold' : 'font-medium text-zinc-400 hover:text-white'}`;
}
});
}
function updateScriptUI() {
if (!optDeva || !optRom) return;
optDeva.className = currentScript === 'deva' ? ACTIVE_BTN : INACTIVE_BTN;
optRom.className = currentScript === 'roman' ? ACTIVE_BTN : INACTIVE_BTN;
}
function updateUiLangUI() {
if (!optUiDeva || !optUiRom || !optUiEn) return;
optUiDeva.className = uiLanguage === 'deva' ? ACTIVE_BTN : INACTIVE_BTN;
optUiRom.className = uiLanguage === 'roman' ? ACTIVE_BTN : INACTIVE_BTN;
optUiEn.className = uiLanguage === 'en' ? ACTIVE_BTN : INACTIVE_BTN;
}
// ============================================================
// CORE LOGIC & TTS ENGINE
// ============================================================
function buildPersonaContext() {
return [userName ? `The user's name is ${userName}; address them by name naturally.` : '', userPersona].filter(Boolean).join(' ');
}
function renderMarkdown(text) {
try {
if (window.marked && typeof window.marked.parse === 'function') return window.marked.parse(text);
} catch {}
return text.replace(/\n/g, '
');
}
function createWelcomeScreen() {
const div = document.createElement('div');
div.id = 'welcome-screen';
div.className = 'flex-1 flex flex-col items-center justify-center select-none text-center';
div.innerHTML = `
Start chatting with Orki AI
`; return div; } function startNewChat() { if (isGenerating && abortController) abortController.abort(); currentChatId = null; currentChatMessages = []; if (chat) { chat.innerHTML = ''; chat.appendChild(createWelcomeScreen()); } toggleDrawer(false); renderHistorySidebar(); } function toggleDrawer(open) { if (!drawer || !drawerBackdrop) return; drawer.style.transform = open ? 'translateX(0%)' : 'translateX(-100%)'; drawerBackdrop.classList.toggle('hidden', !open); } function ensureDevanagariForTTS(text) { if (/[\u0900-\u097F]/.test(text)) return text; return text .toLowerCase() .replace(/\bkhulumbai\b/g, "खुलुमबाय") .replace(/\bmabwi\b/g, "माबोरै") .replace(/\bmabrwi\b/g, "माबोरै") .replace(/\bmabwrwi\b/g, "माबोरै") .replace(/\bmwjang\b/g, "मोजां") .replace(/\bmwzang\b/g, "मोजां") .replace(/\bjwmwi\b/g, "जोम्वै") .replace(/\bdong\b/g, "दं") .replace(/\bdonga\b/g, "दङ") .replace(/\bnwng\b/g, "नों") .replace(/\bnwngha\b/g, "नोंहा") .replace(/\bang\b/g, "आं") .replace(/\bangha\b/g, "आंहा") .replace(/\bma\b/g, "मा") .replace(/\bkhobor\b/g, "खबर") .replace(/\bthang\b/g, "थां") .replace(/\bthangnw\b/g, "थांनो") .replace(/\bkhalam\b/g, "खालाम") .replace(/\bkhalamnw\b/g, "खालामनो") .replace(/\bswr\b/g, "सोर") .replace(/\bboha\b/g, "बहा") .replace(/\bfai\b/g, "फै") .replace(/\bphai\b/g, "फै") .replace(/\bonkham\b/g, "ओंखाम") .replace(/\bwngkham\b/g, "ओंखाम") .replace(/kh/g, "ख").replace(/ph/g, "फ").replace(/th/g, "थ") .replace(/ng/g, "ं") .replace(/wi/g, "ुइ").replace(/ai/g, "ै").replace(/ao/g, "ौ") .replace(/jw/g, "जो").replace(/nw/g, "नो").replace(/bw/g, "बो").replace(/w/g, "ु") .replace(/b/g, "ब").replace(/d/g, "द").replace(/g/g, "ग").replace(/h/g, "ह") .replace(/j/g, "ज").replace(/k/g, "क").replace(/l/g, "ल").replace(/m/g, "म") .replace(/n/g, "न").replace(/p/g, "प").replace(/r/g, "र").replace(/s/g, "स") .replace(/t/g, "त").replace(/y/g, "य").replace(/z/g, "ज") .replace(/a/g, "ा").replace(/i/g, "ि").replace(/u/g, "ु").replace(/e/g, "े").replace(/o/g, "ो"); } function cleanTextForTTS(rawText) { const cleaned = rawText .replace(/[#*_`~>\[\]\(\)]/g, '') .replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{2600}-\u{26FF}]/gu, '') .trim(); return ensureDevanagariForTTS(cleaned); } // ============================================================ // FLOATING AUDIO PILL CONTROLLER // ============================================================ const audioPlayerBar = $('audio-player-bar'); const playerToggleBtn = $('player-toggle-btn'); const playerPlayIcon = $('player-play-icon'); const playerPauseIcon = $('player-pause-icon'); const playerTimer = $('player-timer'); const playerSeeker = $('player-seeker'); const playerCloseBtn = $('player-close-btn'); const playerLoadingIcon = $('player-loading-icon'); function formatAudioTime(seconds) { if (isNaN(seconds)) return "00:00"; const m = Math.floor(seconds / 60).toString().padStart(2, '0'); const s = Math.floor(seconds % 60).toString().padStart(2, '0'); return `${m}:${s}`; } function showPlayerLoading() { if (!audioPlayerBar) return; audioPlayerBar.classList.remove('hidden'); playerPlayIcon?.classList.add('hidden'); playerPauseIcon?.classList.add('hidden'); playerLoadingIcon?.classList.remove('hidden'); if (playerTimer) playerTimer.textContent = "--:--"; if (playerSeeker) { playerSeeker.value = 0; playerSeeker.disabled = true; } if (playerToggleBtn) playerToggleBtn.disabled = true; } function bindAudioToPlayer(audio) { if (!audioPlayerBar) return; audioPlayerBar.classList.remove('hidden'); playerLoadingIcon?.classList.add('hidden'); if (playerSeeker) playerSeeker.disabled = false; if (playerToggleBtn) playerToggleBtn.disabled = false; audio.ontimeupdate = () => { playerTimer.textContent = formatAudioTime(audio.currentTime); if (audio.duration) { playerSeeker.value = (audio.currentTime / audio.duration) * 100; } }; audio.onplay = () => { playerPlayIcon?.classList.add('hidden'); playerPauseIcon?.classList.remove('hidden'); }; audio.onpause = () => { playerPlayIcon?.classList.remove('hidden'); playerPauseIcon?.classList.add('hidden'); }; audio.onended = () => { audioPlayerBar.classList.add('hidden'); if (playerSeeker) playerSeeker.value = 0; if (playerTimer) playerTimer.textContent = "00:00"; if (activeTtsBtn) activeTtsBtn.innerHTML = `🔊 Listen`; currentAudio = null; activeTtsBtn = null; }; } on('player-toggle-btn', 'click', () => { if (!currentAudio) return; if (currentAudio.paused) currentAudio.play(); else currentAudio.pause(); }); if (playerSeeker) { playerSeeker.oninput = () => { if (currentAudio && currentAudio.duration) { currentAudio.currentTime = (playerSeeker.value / 100) * currentAudio.duration; } }; } on('player-close-btn', 'click', () => { if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; currentAudio = null; } if (activeTtsBtn) activeTtsBtn.innerHTML = `🔊 Listen`; activeTtsBtn = null; audioPlayerBar?.classList.add('hidden'); }); async function playBodoSpeech(btn, text) { if (activeTtsBtn === btn && currentAudio && !currentAudio.paused) { currentAudio.pause(); btn.innerHTML = `🔊 Listen`; return; } if (currentAudio) { currentAudio.pause(); if (activeTtsBtn) activeTtsBtn.innerHTML = `🔊 Listen`; } const cleanText = cleanTextForTTS(text); if (!cleanText) return; showPlayerLoading(); if (ttsAudioCache.has(cleanText)) { activeTtsBtn = btn; btn.disabled = false; btn.innerHTML = `⏹ Stop`; if (!currentAudio) currentAudio = new Audio(); currentAudio.src = ttsAudioCache.get(cleanText); bindAudioToPlayer(currentAudio); const cachedBaseOnEnded = currentAudio.onended; currentAudio.onended = () => { cachedBaseOnEnded && cachedBaseOnEnded(); btn.innerHTML = `🔊 Listen`; }; try { await currentAudio.play(); } catch { btn.innerHTML = `⚠️ Retry`; } return; } try { activeTtsBtn = btn; btn.disabled = true; btn.innerHTML = `⏳ Rendering...`; const res = await fetch(TTS_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: cleanText }) }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.error || `Server status ${res.status}`); } const data = await res.json(); const audioUrl = data.audioUrl; if (!audioUrl) throw new Error("Audio URL missing from server response"); ttsAudioCache.set(cleanText, audioUrl); btn.disabled = false; btn.innerHTML = `⏹ Stop`; if (!currentAudio) currentAudio = new Audio(); currentAudio.src = audioUrl; bindAudioToPlayer(currentAudio); const freshBaseOnEnded = currentAudio.onended; currentAudio.onended = () => { freshBaseOnEnded && freshBaseOnEnded(); btn.innerHTML = `🔊 Listen`; }; await currentAudio.play(); } catch (err) { console.error("TTS generation error:", err); btn.disabled = false; btn.innerHTML = `⚠️ Retry`; audioPlayerBar?.classList.add('hidden'); alert(`TTS Failed: ${err.message}`); } } function attachTtsButton(bubble, text) { if (!bubble || !text.trim()) return; const actionRow = document.createElement('div'); actionRow.className = "flex items-center gap-2 mt-2 pt-1 border-t border-zinc-700/30"; const ttsBtn = document.createElement('button'); ttsBtn.type = "button"; ttsBtn.className = "text-[11px] font-medium text-zinc-400 hover:text-emerald-400 flex items-center gap-1 transition cursor-pointer"; ttsBtn.innerHTML = `🔊 Listen`; ttsBtn.onclick = () => playBodoSpeech(ttsBtn, text.trim()); actionRow.appendChild(ttsBtn); bubble.appendChild(actionRow); } function createBubbleElement(role) { const wrapper = document.createElement('div'); wrapper.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`; const bubble = document.createElement('div'); bubble.className = role === 'user' ? 'bg-emerald-600 text-white rounded-2xl rounded-tr-xs px-4 py-2.5 max-w-[85%] text-[15px] whitespace-pre-wrap leading-relaxed shadow-sm break-words [overflow-wrap:anywhere]' : 'bg-[#242427] border border-zinc-700/40 text-zinc-100 rounded-2xl rounded-tl-xs px-4 py-2.5 max-w-[85%] text-[15px] shadow-sm leading-relaxed prose-custom break-words [overflow-wrap:anywhere]'; wrapper.appendChild(bubble); if (chat) { chat.appendChild(wrapper); chat.scrollTop = chat.scrollHeight; } return bubble; } function appendMessage(role, text) { const bubble = createBubbleElement(role); bubble.innerHTML = role === 'user' ? text : renderMarkdown(text); if (role === 'model') attachTtsButton(bubble, text); if (chat) chat.scrollTop = chat.scrollHeight; } async function processStream(reader, onChunk) { const decoder = new TextDecoder('utf-8'); 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 (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data: ')) { const jsonStr = trimmed.slice(6).trim(); if (!jsonStr) continue; try { const parsed = JSON.parse(jsonStr); const chunk = parsed.candidates?.[0]?.content?.parts?.[0]?.text || ''; if (chunk) onChunk(chunk); } catch {} } } } } async function getAuthToken() { if (!auth || !auth.currentUser) return null; try { return await auth.currentUser.getIdToken(); } catch (e) { console.warn("Failed to get ID token:", e); return null; } } async function persistUsageIncrement() { currentDailyUsage++; updateUsageUI(); const today = getTodayString(); if (currentUser && db) { try { await db.collection("users").doc(currentUser.uid).set({ dailyUsage: { date: today, count: currentDailyUsage } }, { merge: true }); } catch (e) { console.warn("Usage sync error:", e); } } else { safeSet('arki_guest_usage', JSON.stringify({ date: today, count: currentDailyUsage })); } } // ============================================================ // DUAL RESPONSE PARSER (display text vs. clean TTS text) // ============================================================ function parseDualResponse(rawText) { if (!rawText.includes('---TTS---')) { return { displayText: rawText.trim(), ttsText: rawText.trim() }; } const [displayPart, ...ttsParts] = rawText.split('---TTS---'); return { displayText: displayPart.trim(), ttsText: ttsParts.join('---TTS---').trim() }; } // ============================================================ // SEND HANDLERS // ============================================================ async function handleSend() { if (isGenerating) { abortController?.abort(); return; } const limit = DAILY_LIMITS[currentPlan] || 20; if (currentDailyUsage >= limit) { if (!currentUser) { alert(`You've used your ${limit} free guest messages for today. Sign in with Google for a higher daily limit.`); openAuthModal(); } else { alert(`You reached your daily limit of ${limit} messages. Upgrade to get higher limits.`); upgradeModal && upgradeModal.classList.remove('hidden'); } return; } const userText = input ? input.value.trim() : ''; if (!userText) return; const welcome = $('welcome-screen'); if (welcome) welcome.remove(); appendMessage('user', userText); currentChatMessages.push({ role: 'user', text: userText }); input.value = ''; input.style.height = 'auto'; setGeneratingState(true); thinking && thinking.classList.remove('hidden'); const scriptConstraint = currentScript === 'roman' ? `\n[Constraint:1. Provide the reply in natural, colloquial Roman Bodo (using 'w', 'dong', and keeping English words like 'Spanish', 'phone' as-is).2. Follow immediately with '---TTS---' on a new line, then the EXACT same sentence in Bodo Devanagari script (बर' हांखो) for speech synthesis.Example format:Oi jwmwi! Spanish raylainw nagirdwng nama? ma khobor nwngna?---TTS---ओइ जोमै! स्पेनिस रायलायनो नागिरदों नामा? मा खबर नोंना?]` : "\n[Constraint: Reply strictly in Bodo Devanagari script (बर' हांखो). Never output Roman script.]"; const depth = MEMORY_DEPTH[currentPlan] || -6; const apiHistory = currentChatMessages.slice(depth).map((msg, index, arr) => ({ role: msg.role === 'user' ? 'user' : 'model', parts: [{ text: (msg.role === 'user' && index === arr.length - 1) ? msg.text + scriptConstraint : msg.text }] })); abortController = new AbortController(); let fullAiText = '', aiBubble = null; try { const idToken = await getAuthToken(); const headers = { 'Content-Type': 'application/json' }; if (idToken) headers['Authorization'] = `Bearer ${idToken}`; const res = await fetch(STREAM_API_URL, { method: 'POST', headers, signal: abortController.signal, body: JSON.stringify({ plan: currentPlan, model: selectedModel, script: currentScript, userPersona: buildPersonaContext(), contents: apiHistory, generationConfig: { temperature: 0.35, maxOutputTokens: selectedModel === 'okafwr-2.1' ? 4000 : 1500 } }) }); if (!res.ok) { if (res.status === 401) throw new Error("Session expired. Please sign in again."); if (res.status === 429) throw new Error("Daily limit reached. Try again tomorrow or upgrade."); const errData = await res.json().catch(() => null) || {}; throw new Error(typeof errData.error?.message === 'object' ? JSON.stringify(errData.error.message) : (errData.error?.message || errData.error || `HTTP ${res.status}`)); } thinking && thinking.classList.add('hidden'); aiBubble = createBubbleElement('model'); await processStream(res.body.getReader(), (chunk) => { fullAiText += chunk; const { displayText } = parseDualResponse(fullAiText); aiBubble.innerHTML = renderMarkdown(displayText); if (chat) chat.scrollTop = chat.scrollHeight; }); if (fullAiText.trim()) { const { displayText, ttsText } = parseDualResponse(fullAiText); aiBubble.innerHTML = renderMarkdown(displayText); currentChatMessages.push({ role: 'model', text: displayText }); await saveCurrentThreadToCloud(); await persistUsageIncrement(); attachTtsButton(aiBubble, ttsText); } } catch (err) { if (err.name === 'AbortError' && fullAiText.trim()) { const { displayText, ttsText } = parseDualResponse(fullAiText); aiBubble.innerHTML = renderMarkdown(displayText); currentChatMessages.push({ role: 'model', text: displayText }); await saveCurrentThreadToCloud(); await persistUsageIncrement(); attachTtsButton(aiBubble, ttsText); } else { thinking && thinking.classList.add('hidden'); let friendlyMsg = err.message; if (err.message.includes('429')) { friendlyMsg = currentScript === 'roman' ? 'Rate limit hit. 15s nena-nwi fin lwir.' : 'रेत लिमिट थफैबाय। 15 सेकेन्द नेनानै फिन लिर।'; } else if (err.message.includes('high demand') || err.message.includes('503')) { friendlyMsg = currentScript === 'roman' ? 'Server-ao traffic baraydwng. 10s unao fin sonanai nay.' : 'सार्भार आव मानसि बारा जादों। 10 सेकेन्द उनाव फिन नाजा।'; } appendMessage('model', friendlyMsg); } } finally { thinking && thinking.classList.add('hidden'); setGeneratingState(false); abortController = null; input && input.focus(); } } async function handleAudioSend(audioBlob, mimeType, liveMode = false) { if (isGenerating) return; const limit = DAILY_LIMITS[currentPlan] || 20; if (currentDailyUsage >= limit) { if (!currentUser) { alert(`You've used your ${limit} free guest messages for today. Sign in with Google for a higher daily limit.`); openAuthModal(); } else { alert(`You reached your daily limit of ${limit} messages. Upgrade to get higher limits.`); upgradeModal && upgradeModal.classList.remove('hidden'); } return; } const welcome = $('welcome-screen'); if (welcome) welcome.remove(); const userBubble = createBubbleElement('user'); userBubble.textContent = '🎙️ Transcribing...'; currentChatMessages.push({ role: 'user', text: '[Voice Note]' }); setGeneratingState(true); thinking && thinking.classList.remove('hidden'); if (liveMode) setLiveTalkUI('thinking'); const base64Audio = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(audioBlob); }); const scriptConstraint = currentScript === 'roman' ? `Constraint:1. Provide the reply in natural, colloquial Roman Bodo (using 'w', 'dong', and keeping English words like 'Spanish', 'phone' as-is).2. Follow immediately with '---TTS---' on a new line, then the EXACT same sentence in Bodo Devanagari script (बर' हांखो) for speech synthesis.Example format:Oi jwmwi! Spanish raylainw nagirdwng nama? ma khobor nwngna?---TTS---ओइ जोमै! स्पेनिस रायलायनो नागिरदों नामा? मा खबर नोंना?` : "Reply strictly in Bodo Devanagari script (बर' हांखो). Never output Roman script."; const depth = MEMORY_DEPTH[currentPlan] || -6; const apiHistory = currentChatMessages.slice(depth).map(msg => ({ role: msg.role === 'user' ? 'user' : 'model', parts: [{ text: msg.text }] })); apiHistory[apiHistory.length - 1] = { role: 'user', parts: [ { inlineData: { mimeType, data: base64Audio } }, { text: `1. Transcribe accurately.\n2. Formulate conversational reply as Orki.\nConstraint: ${scriptConstraint}\n\nOutput MUST strictly follow:\nTRANSCRIPTION:Sign in to sync history
`; clearAllBtn && clearAllBtn.classList.add('hidden'); return; } if (cachedThreads.length === 0) { historyList.innerHTML = `No conversations yet
`; clearAllBtn && clearAllBtn.classList.add('hidden'); return; } clearAllBtn && clearAllBtn.classList.remove('hidden'); historyList.innerHTML = ''; cachedThreads.forEach(thread => { const isActive = thread.id === currentChatId; const row = document.createElement('div'); row.className = `group flex items-center justify-between p-1.5 rounded-lg text-[12px] gap-1 transition ${isActive ? 'bg-zinc-800 text-white font-medium' : 'text-zinc-300 hover:bg-zinc-800/60'}`; const btn = document.createElement('button'); btn.type = "button"; btn.className = "flex-1 text-left truncate cursor-pointer py-0.5 px-1"; btn.textContent = thread.title || "Conversation"; btn.onclick = () => openThread(thread); const delBtn = document.createElement('button'); delBtn.type = "button"; delBtn.className = "text-zinc-500 hover:text-red-400 p-1 rounded hover:bg-zinc-700/50 opacity-70 group-hover:opacity-100 transition cursor-pointer shrink-0"; delBtn.title = "Delete conversation"; delBtn.innerHTML = trashIcon; delBtn.onclick = async (e) => { e.stopPropagation(); try { await db.collection("users").doc(currentUser.uid).collection("threads").doc(thread.id).delete(); if (currentChatId === thread.id) startNewChat(); else await loadUserThreads(currentUser.uid); } catch (err) { console.error("Delete failed:", err); } }; row.appendChild(btn); row.appendChild(delBtn); historyList.appendChild(row); }); } function openThread(thread) { if (isGenerating && abortController) abortController.abort(); currentChatId = thread.id; currentChatMessages = Array.isArray(thread.messages) ? [...thread.messages] : []; if (chat) { chat.innerHTML = ''; if (currentChatMessages.length === 0) chat.appendChild(createWelcomeScreen()); else currentChatMessages.forEach(msg => appendMessage(msg.role, msg.text)); } renderHistorySidebar(); toggleDrawer(false); } // ============================================================ // PAYMENT & SETTINGS // ============================================================ function initiatePayment(planTier, amountPaise) { if (!currentUser) { openAuthModal(); return; } upgradeModal && upgradeModal.classList.add('hidden'); const options = { key: RAZORPAY_KEY_ID, amount: amountPaise, currency: "INR", name: "Orki AI", description: `${planTier} Plan (${selectedCycle})`, prefill: { email: currentUser ? currentUser.email : "", contact: "9999999999" }, theme: { color: planTier === "Pro" ? "#f59e0b" : "#059669" }, handler: async function (response) { try { // NOTE: In production, verify razorpay_signature on Supabase server before updating plan const daysToAdd = selectedCycle === 'yearly' ? 365 : selectedCycle === 'monthly' ? 30 : 7; const expiresAt = new Date(Date.now() + daysToAdd * 24 * 60 * 60 * 1000); await db.collection("users").doc(currentUser.uid).set({ plan: planTier, cycle: selectedCycle, expiresAt: expiresAt, updatedAt: firebase.firestore.FieldValue.serverTimestamp() }, { merge: true }); currentPlan = planTier; userPlan.textContent = `Plan: ${planTier}`; userPlan.className = `text-[11px] font-semibold ${planTier === "Pro" ? 'text-amber-400' : 'text-emerald-400'}`; upgradeBtn.textContent = `${planTier} Active`; upgradeBtn.disabled = true; updateModelLockUI(); updateUsageUI(); alert(`Success! You are now subscribed to ${planTier} until ${expiresAt.toLocaleDateString()}.`); } catch (err) { alert("Activation failed: " + err.message); } } }; new Razorpay(options).open(); } // ============================================================ // EVENT BINDINGS // ============================================================ on('mic-btn', 'click', () => { if (isLiveTalkActive) { stopLiveTalk(); return; } isRecording ? stopRecording() : startRecording(); }); on('menu-btn', 'click', () => toggleDrawer(true)); on('close-drawer', 'click', () => toggleDrawer(false)); on('drawer-backdrop', 'click', () => toggleDrawer(false)); on('new-chat-btn', 'click', startNewChat); on('incognito-btn', 'click', () => { isIncognito = !isIncognito; updateIncognitoUI(); startNewChat(); }); on('model-select-btn', 'click', (e) => { e.stopPropagation(); modelDropdown && modelDropdown.classList.toggle('hidden'); }); document.addEventListener('click', () => modelDropdown && !modelDropdown.classList.contains('hidden') && modelDropdown.classList.add('hidden')); on('select-orki', 'click', () => { selectedModel = 'orki-3.0'; if (currentModelDisplay) { currentModelDisplay.textContent = 'Orki 3.0'; currentModelDisplay.className = 'text-zinc-200'; } modelDropdown && modelDropdown.classList.add('hidden'); }); on('select-okafwr', 'click', () => { if (currentPlan !== 'Pro') { modelDropdown && modelDropdown.classList.add('hidden'); upgradeModal && upgradeModal.classList.remove('hidden'); alert("Okafwr 2.1 requires an active Pro subscription."); return; } selectedModel = 'okafwr-2.1'; if (currentModelDisplay) { currentModelDisplay.textContent = 'Okafwr 2.1 ⚡'; currentModelDisplay.className = 'text-amber-400 font-bold'; } modelDropdown && modelDropdown.classList.add('hidden'); }); on('cycle-weekly', 'click', () => { selectedCycle = 'weekly'; updatePricingUI(); }); on('cycle-monthly', 'click', () => { selectedCycle = 'monthly'; updatePricingUI(); }); on('cycle-yearly', 'click', () => { selectedCycle = 'yearly'; updatePricingUI(); }); on('upgrade-btn', 'click', () => { if (!currentUser) { openAuthModal(); return; } upgradeModal && upgradeModal.classList.remove('hidden'); toggleDrawer(false); }); on('close-upgrade', 'click', () => upgradeModal && upgradeModal.classList.add('hidden')); on('buy-plus-btn', 'click', () => initiatePayment("Plus", PRICING[selectedCycle].plusPaise)); on('buy-pro-btn', 'click', () => initiatePayment("Pro", PRICING[selectedCycle].proPaise)); on('open-settings', 'click', () => { toggleDrawer(false); if (personaInput) personaInput.value = userPersona; if (usernameInput) usernameInput.value = userName; updateScriptUI(); updateUiLangUI(); settingsModal && settingsModal.classList.remove('hidden'); }); on('close-settings', 'click', () => settingsModal && settingsModal.classList.add('hidden')); on('opt-deva', 'click', () => { currentScript = 'deva'; updateScriptUI(); }); on('opt-rom', 'click', () => { currentScript = 'roman'; updateScriptUI(); }); on('opt-ui-deva', 'click', () => { uiLanguage = 'deva'; updateUiLangUI(); applyUILanguage(uiLanguage); }); on('opt-ui-rom', 'click', () => { uiLanguage = 'roman'; updateUiLangUI(); applyUILanguage(uiLanguage); }); on('opt-ui-en', 'click', () => { uiLanguage = 'en'; updateUiLangUI(); applyUILanguage(uiLanguage); }); on('save-settings', 'click', () => { userPersona = personaInput ? personaInput.value.trim() : ''; userName = usernameInput ? usernameInput.value.trim() : ''; safeSet('arki_script', currentScript); safeSet('arki_ui_lang', uiLanguage); safeSet('arki_persona', userPersona); safeSet('arki_username', userName); settingsModal && settingsModal.classList.add('hidden'); }); on('clear-all-btn', 'click', async () => { if (!currentUser || !db || !confirm("Clear all conversations?")) return; try { const snap = await db.collection("users").doc(currentUser.uid).collection("threads").get(); await Promise.all(snap.docs.map(d => d.ref.delete())); startNewChat(); await loadUserThreads(currentUser.uid); } catch (err) { console.error("Clear all failed:", err); } }); if (input) { input.addEventListener('input', function () { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 112) + 'px'; }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); } if (form) { form.onsubmit = (e) => { e.preventDefault(); handleSend(); }; } // ============================================================ // FIREBASE INIT & GOOGLE AUTH // ============================================================ try { if (window.firebase) { firebase.initializeApp(firebaseConfig); auth = firebase.auth(); db = firebase.firestore(); // Catch returning redirect from Google auth.getRedirectResult().then((result) => { if (result && result.user) { if (authModal) authModal.classList.add('hidden'); } }).catch((err) => { console.error("Redirect return error:", err); alert("Login error: " + err.message); }); auth.onAuthStateChanged(async (user) => { currentUser = user; const today = getTodayString(); if (user) { alert("Firebase confirmed login: " + user.email); // <--- DEBUG ALERT if (authModal) authModal.classList.add('hidden'); // <--- FORCE HIDE MODAL userDisplay && (userDisplay.textContent = user.displayName || user.email.split('@')[0]); syncStatus && (syncStatus.textContent = "Synced to Cloud"); authActionBtn && (authActionBtn.textContent = "Logout"); upgradeBtn && upgradeBtn.classList.remove('hidden'); if (input) { input.disabled = false; input.placeholder = "Message Orki..."; } if (sendBtn) sendBtn.disabled = false; if (micBtn) micBtn.disabled = false; if (liveTalkBtn) liveTalkBtn.disabled = false; try { const doc = await db.collection("users").doc(user.uid).get(); if (doc.exists) { const data = doc.data(); const now = new Date(); const expiresAt = data.expiresAt ? data.expiresAt.toDate() : null; if (expiresAt && expiresAt < now) { currentPlan = "Free"; await db.collection("users").doc(user.uid).update({ plan: "Free" }); } else { currentPlan = data.plan || "Free"; } const usageData = data.dailyUsage || {}; if (usageData.date !== today) { currentDailyUsage = 0; await db.collection("users").doc(user.uid).set({ dailyUsage: { date: today, count: 0 } }, { merge: true }); } else { currentDailyUsage = usageData.count || 0; } } else { currentPlan = "Free"; currentDailyUsage = 0; } } catch { currentPlan = "Free"; currentDailyUsage = 0; } if (userPlan) { userPlan.textContent = `Plan: ${currentPlan}`; userPlan.className = `text-[11px] font-semibold ${currentPlan === "Pro" ? 'text-amber-400' : 'text-emerald-400'}`; if (upgradeBtn) { upgradeBtn.textContent = currentPlan === "Pro" ? "Pro Active" : currentPlan === "Plus" ? "Plus Active" : "Upgrade"; upgradeBtn.disabled = currentPlan === "Pro"; } } updateModelLockUI(); updateUsageUI(); loadUserThreads(user.uid); } else { userDisplay && (userDisplay.textContent = "Guest User"); syncStatus && (syncStatus.textContent = "Local Session (not saved)"); authActionBtn && (authActionBtn.textContent = "Login"); if (userPlan) { userPlan.textContent = "Plan: Guest"; userPlan.className = "text-[11px] font-semibold text-zinc-400"; } upgradeBtn && upgradeBtn.classList.add('hidden'); currentPlan = "Guest"; selectedModel = 'orki-3.0'; if (currentModelDisplay) { currentModelDisplay.textContent = 'Orki 3.0'; currentModelDisplay.className = 'text-zinc-200'; } // Guest quota lives in localStorage only — no cloud sync, resets daily try { const guestUsage = JSON.parse(safeGet('arki_guest_usage', 'null')) || {}; currentDailyUsage = guestUsage.date === today ? (guestUsage.count || 0) : 0; } catch { currentDailyUsage = 0; } updateModelLockUI(); updateUsageUI(); cachedThreads = []; // Guests can chat freely — only cloud sync/history/upgrades require sign-in if (input) { input.disabled = false; input.placeholder = "Message Orki... (sign in to save history)"; } if (sendBtn) sendBtn.disabled = false; if (micBtn) micBtn.disabled = false; if (liveTalkBtn) liveTalkBtn.disabled = false; renderHistorySidebar(); } }); } } catch (e) { console.warn("Firebase restricted:", e); } on('auth-action-btn', 'click', () => { if (currentUser && auth) { auth.signOut(); } else { openAuthModal(); toggleDrawer(false); } }); on('auth-close-btn', 'click', () => authModal && authModal.classList.add('hidden')); // Google Auth Handler (Redirect mode for mobile reliability) on('auth-google-btn', 'click', async () => { if (!auth) { alert("Firebase not ready. Please wait a second and try again."); return; } const provider = new firebase.auth.GoogleAuthProvider(); provider.setCustomParameters({ prompt: 'select_account' }); try { // Navigates the entire browser tab cleanly — no dropped popups await auth.signInWithRedirect(provider); } catch (err) { alert("Redirect trigger error: " + err.message); } }); // ============================================================ // INIT // ============================================================ // Guests can chat right away — Firebase auth state resolves shortly // after and will upgrade the UI to a signed-in view if applicable. currentPlan = "Guest"; try { const guestUsage = JSON.parse(safeGet('arki_guest_usage', 'null')) || {}; currentDailyUsage = guestUsage.date === getTodayString() ? (guestUsage.count || 0) : 0; } catch { currentDailyUsage = 0; } if (input) { input.disabled = false; input.placeholder = "Message Orki... (sign in to save history)"; } if (sendBtn) sendBtn.disabled = false; if (micBtn) micBtn.disabled = false; if (liveTalkBtn) liveTalkBtn.disabled = false; updateScriptUI(); updateUiLangUI(); updatePricingUI(); updateUsageUI(); updateIncognitoUI(); applyUILanguage(uiLanguage); // ============================================================ // EDGE FUNCTION WARM-UP PING // ============================================================ function warmUpEdgeFunctions() { const targets = [STREAM_API_URL, TTS_API_URL]; targets.forEach(url => { fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ping: true, warmup: true }) }).catch(() => {}); // Catch silently; all we care about is forcing Deno to spin up }); } // 1. Wake them up immediately on page load warmUpEdgeFunctions(); // 2. Keep containers warm every 4 minutes while the tab is open setInterval(warmUpEdgeFunctions, 4 * 60 * 1000);