Secrypt

Can you keep a secret?

Private, uncensored AI chat.

Drop image to attach

Secrypt can make mistakes. Check important info.

Also from us: FileShot · Graysoft · Embed Chat privately

Private, uncensored AI chat

Secrypt is private AI chat for questions you would not post publicly. No training on your messages. No ad targeting from your prompts. Start in the composer above - this section is here for humans who scroll and for search engines that need clear copy.

What Secrypt is

Secrypt is a browser-based private AI chat product. It is built for discretion: uncensored answers within legal limits, local history controls, and a quiet interface without the surveillance vibe of mainstream consumer bots.

Why a hybrid homepage

The chat UI above is the product. The guides below build topical authority around private AI chat, uncensored assistants, and privacy practices - so Google and AI search engines can understand Secrypt beyond an empty app shell.

Private AI guides (topical cluster)

Start with the cluster, then open a discreet thread when you are ready.

Private AI chat Uncensored AI chat Private ChatGPT alternative AI chat with no training Anonymous AI chat Secure AI assistant Local AI vs cloud AI ChatGPT privacy concerns Uncensored LLM Private AI for work AI privacy best practices Encrypted AI chat Best private AI chatbot Does ChatGPT train on my chats Uncensored AI chat online Private AI chat no account AI chat for sensitive topics Private LLM chat ChatGPT vs private AI Use AI without sharing data Confidential AI assistant Private AI chat for developers Is my AI chat private Private AI journaling Secrypt vs Venice AI Secrypt vs Proton Lumo Secrypt vs Duck.ai Secrypt vs ChatGPT privacy Best private uncensored AI 2026 Venice AI pricing vs Secrypt Invite & Earn Uncensored AI without crypto casino Cipher API OpenAI-compatible Use Secrypt in Cursor Private uncensored AI API Local LLM API vs hosted Continue.dev + Secrypt VS Code private OpenAI backend Secrypt API pricing explained OpenAI API alternative no training Best OpenAI-compatible API for Cursor AI that does not train on your data Zero retention AI chat AI data leakage prevention Uncensored AI chatbot OpenAI compatible private API AI privacy policy checklist ChatGPT alternative without selling data Private coding assistant Guides hub Private AI guide AI privacy guide Uncensored AI guide Private AI API guide Use cases hub Alternatives hub Compare hub Use case: email drafting ChatGPT alternative Private vs mainstream AI Private AI for students Private AI for freelancers Private AI for engineers Private AI email drafting ChatGPT privacy alternative Cipher API quickstart Verify AI privacy policy Migrate from ChatGPT to Secrypt Uncensored AI for writing Private AI no data selling Cipher API API docs

Browse all Secrypt blog guides · Guides · Pricing · API · Invite & Earn · About · Privacy Policy

Frequently asked questions

What is Secrypt?

Secrypt is private, uncensored AI chat in the browser. Conversations are not used to train models or build ad profiles.

Does Secrypt train on my chats?

No. Secrypt does not use your conversation content to train AI models.

Is Secrypt free?

Yes. There is a free tier. Paid plans unlock higher limits after a trial when you upgrade.

Does Secrypt have an API?

Yes. Cipher API is OpenAI-compatible at secrypt.space/v1. Plans include a daily API allowance; after that it is pay-as-you-go at $0.01 per request from prepaid credits.

Is chat-as-homepage bad for SEO?

Not if the HTML also includes crawlable explanations, FAQs, and internal links - which is what this section provides under the same URL.

External references

For general privacy hygiene, see guidance from FTC privacy and security resources and browser security basics from MDN Web Security.

© 2026 Secrypt · [email protected]

'; } if (/|]/i.test(code)) return code; return '' + code + ''; } function bindCodeBlockActions(root) { if (!root) return; root.querySelectorAll('.code-block').forEach(function (block) { if (block.dataset.bound === '1') return; block.dataset.bound = '1'; block.addEventListener('click', async function (e) { const btn = e.target.closest('[data-code-act]'); if (!btn || !block.contains(btn)) return; e.preventDefault(); const act = btn.getAttribute('data-code-act'); const lang = block.getAttribute('data-code-lang') || 'text'; const text = getCodeBlockText(block); if (act === 'copy') { try { await navigator.clipboard.writeText(text); btn.classList.add('is-ok'); setTimeout(function () { btn.classList.remove('is-ok'); }, 1200); } catch { btn.classList.remove('is-ok'); } return; } if (act === 'download') { downloadTextFile('secrypt.' + codeExtForLang(lang), text); return; } if (act === 'play') { const pane = block.querySelector('.code-preview'); const frame = pane && pane.querySelector('iframe'); if (!pane || !frame) return; const open = !block.classList.contains('is-previewing'); if (open) { frame.srcdoc = previewSrcdoc(lang, text); pane.hidden = false; pane.classList.add('open'); block.classList.add('is-previewing'); btn.classList.add('is-on'); btn.setAttribute('data-code-act', 'play'); btn.setAttribute('title', 'Close preview'); btn.setAttribute('aria-label', 'Close preview'); btn.innerHTML = svgIcon('close'); } else { frame.srcdoc = ''; pane.classList.remove('open'); pane.hidden = true; block.classList.remove('is-previewing'); btn.classList.remove('is-on'); btn.setAttribute('title', 'Play preview'); btn.setAttribute('aria-label', 'Play preview'); btn.innerHTML = svgIcon('play'); } } }); }); } function clearMessageQueue() { messageQueue = []; if (drainTimer) { clearTimeout(drainTimer); drainTimer = null; } renderMessageQueue(); } function addQueuedMessage(text) { const t = String(text || '').trim(); if (!t) return; messageQueue.push({ id: Date.now() + Math.random(), text: t }); renderMessageQueue(); } function removeQueuedMessage(id) { messageQueue = messageQueue.filter(function (m) { return m.id !== id; }); renderMessageQueue(); } function updateQueuedMessage(id, text) { messageQueue = messageQueue.map(function (m) { return m.id === id ? { id: m.id, text: text } : m; }); } function renderMessageQueue() { const box = els.messageQueue; const list = els.messageQueueList; const label = els.messageQueueLabel; if (!box || !list || !label) return; if (!messageQueue.length) { box.hidden = true; box.classList.remove('open'); list.innerHTML = ''; label.textContent = 'Queue (0)'; return; } box.hidden = false; box.classList.add('open'); label.textContent = 'Queue (' + messageQueue.length + ')'; list.innerHTML = messageQueue.map(function (m, i) { return ( '
' + '' + (i + 1) + '' + '' + '' + '' + '
' ); }).join(''); list.querySelectorAll('.message-queue-row').forEach(function (row) { const id = Number(row.getAttribute('data-qid')); const input = row.querySelector('input'); if (input) { input.addEventListener('input', function () { updateQueuedMessage(id, input.value); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); forceSendQueued(id); } }); } row.querySelectorAll('[data-q-act]').forEach(function (btn) { btn.addEventListener('click', function () { const act = btn.getAttribute('data-q-act'); if (act === 'remove') removeQueuedMessage(id); if (act === 'now') forceSendQueued(id); }); }); }); } function abortActiveStream() { if (activeAbort) { try { activeAbort.abort(); } catch { /* */ } } } function scheduleQueueDrain() { if (skipQueueDrain) { skipQueueDrain = false; return; } if (streaming || !messageQueue.length) return; if (drainTimer) clearTimeout(drainTimer); drainTimer = setTimeout(function () { drainTimer = null; if (streaming || !messageQueue.length) return; const next = messageQueue[0]; removeQueuedMessage(next.id); sendMessage(next.text, { fromQueue: true }); }, 500); } async function forceSendQueued(id) { const item = messageQueue.find(function (m) { return m.id === id; }); if (!item) return; const text = String(item.text || '').trim(); removeQueuedMessage(id); if (!text) return; skipQueueDrain = true; if (streaming) { abortActiveStream(); const start = Date.now(); while (streaming && Date.now() - start < 4000) { await new Promise(function (r) { setTimeout(r, 40); }); } } await sendMessage(text, { fromQueue: true, force: true }); } function openImageLightbox(src) { const box = document.getElementById('imgLightbox'); const img = document.getElementById('imgLightboxImg'); const dl = document.getElementById('imgLightboxDownload'); if (!box || !img) return; img.src = src; if (dl) { dl.href = src; dl.download = 'secrypt-image.png'; } box.classList.add('open'); } function renderMessages() { const chat = getChat(); const conv = getActive(); const char = getCharacter(conv && conv.characterId); const chatMenuBtn = document.getElementById('chatMenuBtn'); if (chatMenuBtn) { chatMenuBtn.classList.toggle('show', !!(conv && chat.length && !conv.ghost)); } if (!chat.length) { els.messages.innerHTML = ''; els.messages.appendChild(els.empty); els.empty.style.display = ''; renderCharPins(); renderCharActiveBanner(); return; } els.empty.style.display = 'none'; if (els.empty.parentNode === els.messages) els.empty.remove(); els.messages.innerHTML = ''; chat.forEach(function (m, msgIdx) { const row = document.createElement('div'); row.className = 'msg ' + m.role; row.dataset.msgIdx = String(msgIdx); const text = messagePlainText(m.content); const imgs = messageImages(m); let imgHtml = ''; if (imgs.length) { imgHtml = '
' + imgs.map(function (a, ii) { const variantBtn = a.generated && !m.streaming ? '' : ''; return '
' + '' + (a.generated ? 'Generated' : 'Attachment') + '' + variantBtn + '
'; }).join('') + '
'; } const timeHtml = settings.showTimestamps && m.createdAt ? '
' + escapeHtml(formatMsgTime(m.createdAt)) + '
' : ''; if (m.upgradeBubble) { row.className = 'msg assistant upgrade-bubble-row'; row.innerHTML = avatarHtmlForCharacter(null) + '
' + '
' + escapeHtml(text || '') + '
' + '
' + '' + '' + (currentUser ? '' : '') + '' + '
' + '
3-day free trial · cancel anytime · credits apply at checkout
' + '
'; bindUpgradeBubbleButtons(row); } else if (m.role === 'assistant') { const statusHtml = m.status ? '
' + escapeHtml(m.status) + '
' : ''; let morphHtml = ''; if (m.generatingImage && !(m.images && m.images.length)) { if (!m.morphStyle) m.morphStyle = pickMorphStyle(); morphHtml = '
Generating…
'; } const canRegen = !m.streaming && !m.upgradeBubble; const canCopy = !m.streaming; const actions = []; if (canCopy) actions.push(msgIconBtn('copy', 'Copy', 'copy', msgIdx)); if (canRegen) actions.push(msgIconBtn('regen', 'Regenerate', 'regen', msgIdx)); if (canRegen) actions.push(msgIconBtn('fork', 'Fork chat here', 'fork', msgIdx)); if (canRegen) actions.push(msgIconBtn('checkpoint', 'Save checkpoint', 'checkpoint', msgIdx)); if (canRegen) actions.push(msgIconBtn('restore', 'Restore to here', 'checkpoint', msgIdx)); if (canCopy) actions.push(msgIconBtn('zip', 'Download all code as zip', 'zip', msgIdx)); const regenHtml = actions.length ? '
' + actions.join('') + '
' : ''; row.innerHTML = avatarHtmlForCharacter(char) + '
' + statusHtml + morphHtml + imgHtml + simpleMarkdown(text || '', { streaming: !!m.streaming }) + regenHtml + timeHtml + '
'; const morphCanvas = row.querySelector('[data-morph] canvas'); if (morphCanvas) startImageMorph(morphCanvas, m.morphStyle); bindCodeBlockActions(row); } else { const actions = []; if (!streaming) { actions.push(msgIconBtn('copy', 'Copy', 'copy', msgIdx)); actions.push(msgIconBtn('edit', 'Edit', 'edit', msgIdx)); actions.push(msgIconBtn('fork', 'Fork chat here', 'fork', msgIdx)); actions.push(msgIconBtn('checkpoint', 'Save checkpoint', 'checkpoint', msgIdx)); actions.push(msgIconBtn('restore', 'Restore to here', 'checkpoint', msgIdx)); } const editHtml = actions.length ? '
' + actions.join('') + '
' : ''; row.innerHTML = '
' + imgHtml + '
' + escapeHtml(text || '') + '
' + editHtml + timeHtml + '
'; } row.querySelectorAll('.msg-images img').forEach(function (img) { img.addEventListener('click', function () { openImageLightbox(img.src); }); }); row.querySelectorAll('[data-img-act="variant"]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); if (streaming) return; sendMessage('Generate a variation of the last image you made — same subject, different composition and lighting. Keep it high quality.'); }); }); row.querySelectorAll('[data-msg-act]').forEach(function (btn) { btn.addEventListener('click', async function () { const act = btn.getAttribute('data-msg-act'); const idx = Number(btn.getAttribute('data-msg-idx')); if (act === 'edit') beginEditUserMessage(idx); if (act === 'regen') regenerateAssistantAt(idx); if (act === 'fork') forkConversationAt(idx); if (act === 'checkpoint') saveCheckpointAt(idx); if (act === 'restore') restoreCheckpointAt(idx); if (act === 'zip') { const files = collectCodeBlocksFromMessage(idx); if (!files.length) return; if (files.length === 1) downloadTextFile(files[0].name, files[0].text); else downloadZipStore('secrypt-code.zip', files); } if (act === 'copy') { const chat = getChat(); const msg = chat[idx]; const plain = msg ? messagePlainText(msg.content) : ''; try { await navigator.clipboard.writeText(plain || ''); btn.classList.add('is-ok'); setTimeout(function () { btn.classList.remove('is-ok'); }, 1200); } catch { /* ignore */ } } }); }); els.messages.appendChild(row); }); els.messagesWrap.scrollTop = els.messagesWrap.scrollHeight; } function beginEditUserMessage(idx) { if (streaming) return; const chat = getChat(); const m = chat[idx]; if (!m || m.role !== 'user') return; const row = els.messages.querySelector('.msg.user[data-msg-idx="' + idx + '"]'); if (!row) return; const bubble = row.querySelector('.bubble'); if (!bubble) return; const current = messagePlainText(m.content); bubble.innerHTML = '
' + '' + '
' + '' + '' + '
' + '
'; const ta = bubble.querySelector('textarea'); ta.value = current; ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); bubble.querySelector('[data-edit-act="cancel"]').addEventListener('click', function () { renderMessages(); }); bubble.querySelector('[data-edit-act="save"]').addEventListener('click', function () { const next = String(ta.value || '').trim(); if (!next) return; submitEditedUserMessage(idx, next); }); } async function submitEditedUserMessage(idx, text) { if (streaming) return; const chat = getChat(); if (!chat[idx] || chat[idx].role !== 'user') return; chat[idx].content = text; chat[idx].images = []; chat.splice(idx + 1); persistChat(); renderMessages(); await sendMessage(text, { skipUserPush: true, editResubmit: true }); } async function regenerateAssistantAt(assistantIdx) { if (streaming) return; const chat = getChat(); const a = chat[assistantIdx]; if (!a || a.role !== 'assistant' || a.upgradeBubble) return; let userIdx = -1; for (let i = assistantIdx - 1; i >= 0; i--) { if (chat[i].role === 'user') { userIdx = i; break; } } if (userIdx < 0) return; const userMsg = chat[userIdx]; chat.splice(userIdx + 1); persistChat(); renderMessages(); const imgs = Array.isArray(userMsg.images) ? userMsg.images.map(function (im) { return { dataUrl: im.dataUrl || im }; }).filter(function (im) { return !!im.dataUrl; }) : []; await sendMessage(messagePlainText(userMsg.content), { skipUserPush: true, images: imgs, regenerate: true, }); } function forkConversationAt(msgIdx) { if (streaming) return; const parent = getActive(); if (!parent || !Array.isArray(parent.messages)) return; const idx = Math.max(0, Math.min(msgIdx, parent.messages.length - 1)); const prefix = parent.messages.slice(0, idx + 1).map(function (m) { return JSON.parse(JSON.stringify(m)); }); const fork = { id: uid(), title: (parent.title || 'Chat') + ' (fork)', messages: prefix, updatedAt: Date.now(), ghost: !!parent.ghost || !!ghostSession, characterId: parent.characterId || null, projectId: parent.projectId || null, projectInstructions: parent.projectInstructions || null, forkedFromId: parent.id, forkedAtMsgIdx: idx, checkpoints: [], }; conversations.unshift(fork); activeId = fork.id; saveConversations(); renderHistory(); renderMessages(); recordTrustEvent({ kind: 'fork', from: parent.id, at: idx }); } function saveCheckpointAt(msgIdx) { if (streaming) return; const conv = getActive(); if (!conv) return; if (!Array.isArray(conv.checkpoints)) conv.checkpoints = []; const label = window.prompt('Checkpoint label (optional)', 'Checkpoint @ ' + (msgIdx + 1)); if (label === null) return; conv.checkpoints.push({ id: uid(), msgIdx: msgIdx, label: String(label || '').trim().slice(0, 80), createdAt: Date.now(), messageCount: msgIdx + 1, }); persistChat(); recordTrustEvent({ kind: 'checkpoint_save', msgIdx: msgIdx }); } function restoreCheckpointAt(msgIdx) { if (streaming) return; const conv = getActive(); if (!conv || !Array.isArray(conv.messages)) return; const idx = Math.max(0, Math.min(msgIdx, conv.messages.length - 1)); if (idx >= conv.messages.length - 1) return; if (!window.confirm('Restore to this message? A fork keeps the current full thread so nothing is lost.')) return; const fork = { id: uid(), title: (conv.title || 'Chat') + ' (before restore)', messages: conv.messages.map(function (m) { return JSON.parse(JSON.stringify(m)); }), updatedAt: Date.now(), ghost: !!conv.ghost, characterId: conv.characterId || null, projectId: conv.projectId || null, projectInstructions: conv.projectInstructions || null, forkedFromId: conv.id, forkedAtMsgIdx: conv.messages.length - 1, checkpoints: Array.isArray(conv.checkpoints) ? conv.checkpoints.slice() : [], }; conversations.unshift(fork); const discarded = conv.messages.length - (idx + 1); conv.messages = conv.messages.slice(0, idx + 1); if (!Array.isArray(conv.checkpoints)) conv.checkpoints = []; conv.checkpoints.push({ id: uid(), msgIdx: idx, label: 'Restored here', createdAt: Date.now(), discardedCount: discarded, }); conv.updatedAt = Date.now(); saveConversations(); renderHistory(); renderMessages(); recordTrustEvent({ kind: 'checkpoint_restore', msgIdx: idx, discarded: discarded, forkId: fork.id }); } function persistChat() { const conv = getActive(); if (!conv) return; conv.updatedAt = Date.now(); const firstUser = conv.messages.find((m) => m.role === 'user'); if (firstUser) conv.title = titleFromMessage(messagePlainText(firstUser.content) || (firstUser.images && firstUser.images.length ? "Image" : "")); saveConversations(); renderHistory(); } async function refreshAuth() { try { const me = await fetch(apiUrl('/api/auth/me'), { credentials: 'include', headers: authHeaders(), }).then((r) => r.json()); if (me.success && me.user) { currentUser = me.user; quota.plan = me.user.license?.plan || 'free'; const sc = await fetch(apiUrl('/api/auth/session-check'), { credentials: 'include' }).then((r) => r.json()); if (sc.authenticated && sc.token) persistAuthToken(sc.token); } else { currentUser = null; persistAuthToken(null); quota.plan = 'anonymous'; } } catch { currentUser = null; } updateQuotaUI(); updateGuestIdentityUi(); await refreshQuotaFromServer(); } els.accountBtn.addEventListener('click', () => { if (currentUser) openAccountModal(); else openAuthModal('signin'); }); els.upgradeBtn.addEventListener('click', () => openUpgradeModal()); els.inviteBtn.addEventListener('click', () => openInviteModal()); els.inviteClose.addEventListener('click', () => els.inviteModal.classList.remove('open')); els.inviteCloseOut.addEventListener('click', () => els.inviteModal.classList.remove('open')); els.inviteSignIn.addEventListener('click', () => { els.inviteModal.classList.remove('open'); openAuthModal('register'); }); els.inviteCopy.addEventListener('click', async () => { try { await navigator.clipboard.writeText(els.inviteLink.value || ''); els.inviteCopy.textContent = 'Copied'; setTimeout(() => { els.inviteCopy.textContent = 'Copy'; }, 1500); } catch { els.inviteLink.select(); } }); async function captureRefFromUrl() { const params = new URLSearchParams(location.search); const ref = (params.get('ref') || '').trim().toLowerCase(); if (!ref) return; try { localStorage.setItem('secrypt_ref', ref); } catch { /* ignore */ } try { await fetch(apiUrl('/api/referrals/capture?ref=' + encodeURIComponent(ref)), { credentials: 'include', }); } catch { /* ignore */ } cleanUrlParams(['ref']); } async function claimPendingReferral() { if (!authToken) return; let ref = ''; try { ref = localStorage.getItem('secrypt_ref') || ''; } catch { /* */ } try { const res = await fetch(apiUrl('/api/referrals/claim'), { method: 'POST', credentials: 'include', headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ ref }), }); const data = await res.json().catch(() => ({})); if (data && data.success) { try { localStorage.removeItem('secrypt_ref'); } catch { /* */ } } } catch { /* ignore */ } } async function syncReferralRewards() { if (!authToken) return; try { await fetch(apiUrl('/api/referrals/sync'), { method: 'POST', credentials: 'include', headers: authHeaders(), }); } catch { /* ignore */ } } async function openInviteModal() { els.inviteError.textContent = ''; els.inviteModal.classList.add('open'); if (!authToken) { els.inviteSignedOut.hidden = false; els.inviteSignedIn.hidden = true; return; } els.inviteSignedOut.hidden = true; els.inviteSignedIn.hidden = false; try { await syncReferralRewards(); const res = await fetch(apiUrl('/api/referrals/me'), { credentials: 'include', headers: authHeaders(), }); const data = await res.json(); if (!data.success) throw new Error(data.error || 'Could not load invite'); els.inviteLink.value = data.invite_url || ('https://secrypt.space/?ref=' + (data.referral_code || '')); els.inviteBalance.textContent = 'Credit balance: ' + dollars(data.credit_balance_cents); const st = data.stats || {}; els.inviteStats.textContent = 'Invites: ' + (st.pending || 0) + ' pending · ' + (st.rewarded || 0) + ' rewarded'; await refreshCreditsUi(); } catch (err) { els.inviteError.textContent = err.message || 'Could not load invite'; } } els.authClose.addEventListener('click', () => els.authModal.classList.remove('open')); els.upgradeClose.addEventListener('click', () => els.upgradeModal.classList.remove('open')); els.accountClose.addEventListener('click', () => els.accountModal.classList.remove('open')); els.accountUpgrade.addEventListener('click', () => openUpgradeModal()); els.tabSignIn.addEventListener('click', () => setAuthMode('signin')); els.tabRegister.addEventListener('click', () => setAuthMode('register')); els.upgradeModal.querySelectorAll('[data-plan]').forEach((btn) => { btn.addEventListener('click', () => startCheckout(btn.getAttribute('data-plan'))); }); els.authForm.addEventListener('submit', async (e) => { e.preventDefault(); els.authError.textContent = ''; els.authSubmit.disabled = true; const email = els.authEmail.value.trim(); const password = els.authPassword.value; try { const path = authMode === 'register' ? '/api/auth/register' : '/api/auth/login'; const body = { email, password }; const res = await fetch(apiUrl(path), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success) throw new Error(data.error || 'Authentication failed'); if (data.token) persistAuthToken(data.token); await refreshAuth(); if (authMode === 'register') await claimPendingReferral(); els.authModal.classList.remove('open'); if (pendingCheckoutPlan) { const plan = pendingCheckoutPlan; pendingCheckoutPlan = null; openUpgradeModal(plan); } else if (window.gtag) { gtag('event', 'login', { method: authMode }); } } catch (err) { els.authError.textContent = err.message || 'Something went wrong'; } finally { els.authSubmit.disabled = false; } }); els.accountLogout.addEventListener('click', async () => { try { await fetch(apiUrl('/api/auth/logout'), { method: 'POST', credentials: 'include' }); } catch { /* ignore */ } persistAuthToken(null); currentUser = null; await refreshAuth(); closeAllModals(); }); window.addEventListener('message', async (ev) => { if (ev.origin !== location.origin) return; if (ev.data && ev.data.type === 'secrypt-checkout' && ev.data.status === 'success') { if (checkoutPopup && !checkoutPopup.closed) checkoutPopup.close(); await refreshAuth(); closeAllModals(); } }); async function handleBootParams() { await captureRefFromUrl(); const params = new URLSearchParams(location.search); const guideToken = params.get('guide_token'); if (guideToken) { await acceptToken(guideToken); cleanUrlParams(['guide_token']); await refreshAuth(); await claimPendingReferral(); try { const pending = sessionStorage.getItem('secrypt-pending-plan'); if (pending === 'pro' || pending === 'unlimited') { sessionStorage.removeItem('secrypt-pending-plan'); pendingCheckoutPlan = pending; } } catch { /* ignore */ } } const checkout = params.get('checkout'); const upgrade = params.get('upgrade'); const plan = params.get('plan'); const credits = params.get('credits'); const creditSession = params.get('session_id'); if (credits === 'success') { await refreshAuth(); if (creditSession) await confirmCreditsSession(creditSession); await refreshCreditsUi(); closeAllModals(); if (currentUser) openAccountModal(); cleanUrlParams(['credits', 'session_id']); } else if (credits === 'cancel') { cleanUrlParams(['credits', 'session_id']); } else if (checkout === 'success') { if (window.opener && window.opener !== window) { try { window.opener.postMessage({ type: 'secrypt-checkout', status: 'success', plan }, location.origin); } catch { /* ignore */ } window.close(); return; } await refreshAuth(); await syncReferralRewards(); closeAllModals(); cleanUrlParams(['checkout', 'plan', 'upgraded']); if (window.gtag) gtag('event', 'purchase', { item_name: plan || 'secrypt' }); } else if (checkout === 'cancel') { cleanUrlParams(['checkout', 'plan']); openUpgradeModal(plan || undefined); } else if (upgrade === 'pro' || upgrade === 'unlimited' || plan === 'pro' || plan === 'unlimited') { cleanUrlParams(['upgrade', 'plan', 'checkout']); openUpgradeModal(upgrade || plan); } else if (params.get('invite') === '1' || params.get('invite') === 'earn') { cleanUrlParams(['invite']); openInviteModal(); } } function autoGrow() { els.input.style.height = 'auto'; els.input.style.height = Math.min(els.input.scrollHeight, 200) + 'px'; // Keep Send enabled while streaming so users can queue follow-ups (Guide parity). els.sendBtn.disabled = !els.input.value.trim(); } function applyQuotaHeaders(res) { const used = res.headers.get('X-Proxy-Used'); const limitHdr = res.headers.get('X-Proxy-Limit'); const plan = res.headers.get('X-Proxy-Plan'); const reset = res.headers.get('X-Proxy-Reset'); if (used != null) quota.used = Number(used) || 0; if (limitHdr != null) quota.limit = limitHdr === 'unlimited' ? Infinity : Number(limitHdr); if (plan) quota.plan = plan; if (reset) quota.resetAt = reset; else if (!quota.resetAt) quota.resetAt = nextUtcMidnightIso(); updateQuotaUI(); } function applyQuotaPayload(data) { if (!data || typeof data !== 'object') return; if (data.used != null) quota.used = Number(data.used) || 0; if (data.limit === null) quota.limit = Infinity; else if (data.limit != null) quota.limit = Number(data.limit); if (data.plan) quota.plan = data.plan; if (data.resetAt) quota.resetAt = data.resetAt; updateQuotaUI(); } async function refreshQuotaFromServer() { try { const headers = {}; if (authToken) headers.Authorization = `Bearer ${authToken}`; const res = await fetch(apiUrl('/api/ai/quota'), { method: 'GET', credentials: 'include', headers, cache: 'no-store', }); applyQuotaHeaders(res); const data = await res.json().catch(() => null); if (data && data.success) applyQuotaPayload(data); } catch { /* keep last known quota */ } updateQuotaUI(); } function getSpeechRecognition() { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; return SR ? new SR() : null; } function stopDictation() { dictating = false; els.micBtn.classList.remove('recording'); if (recognition) { try { recognition.stop(); } catch { /* */ } recognition = null; } } function toggleDictation() { if (dictating) { stopDictation(); return; } const SR = getSpeechRecognition(); if (!SR) { alert('Speech recognition is not supported in this browser.'); return; } stopVoiceMode(); recognition = SR; recognition.continuous = true; recognition.interimResults = true; recognition.lang = 'en-US'; dictating = true; els.micBtn.classList.add('recording'); let base = els.input.value; recognition.onresult = (e) => { let interim = ''; let final = ''; for (let i = e.resultIndex; i < e.results.length; i++) { const t = e.results[i][0].transcript; if (e.results[i].isFinal) final += t; else interim += t; } els.input.value = (base + final + interim).trimStart(); autoGrow(); }; recognition.onerror = () => stopDictation(); recognition.onend = () => { if (dictating) { try { recognition.start(); } catch { stopDictation(); } } }; try { recognition.start(); } catch { stopDictation(); } } let ttsChain = Promise.resolve(); let ttsBuf = ''; let ttsActive = false; let cachedVoices = null; function setVoiceUi(state) { const form = els.composer || document.getElementById('form'); if (!form) return; form.classList.toggle('voice-active', !!voiceMode); form.classList.toggle('voice-listening', state === 'listening'); form.classList.toggle('voice-speaking', state === 'speaking'); els.voiceBtn.classList.toggle('voice-on', !!voiceMode); els.voiceBtn.classList.toggle('speaking', state === 'speaking'); if (els.input) { els.input.placeholder = voiceMode ? (state === 'speaking' ? 'Secrypt is speaking...' : 'Listening... speak now') : 'Message Secrypt...'; } } function unlockSpeech() { if (!window.speechSynthesis) return; try { const warm = new SpeechSynthesisUtterance(' '); warm.volume = 0; window.speechSynthesis.speak(warm); window.speechSynthesis.cancel(); } catch (e) { /* ignore */ } } function loadVoices() { return new Promise(function (resolve) { if (!window.speechSynthesis) { resolve([]); return; } function ready() { cachedVoices = window.speechSynthesis.getVoices() || []; resolve(cachedVoices); } var existing = window.speechSynthesis.getVoices(); if (existing && existing.length) { cachedVoices = existing; resolve(existing); return; } window.speechSynthesis.onvoiceschanged = function () { ready(); }; setTimeout(ready, 600); }); } function pickVoice(voices) { var list = voices || cachedVoices || []; var rank = [ /microsoft aria.*natural/i, /microsoft jenny.*natural/i, /microsoft guy.*natural/i, /google us english/i, /samantha/i, /natural|neural|premium/i, ]; for (var i = 0; i < rank.length; i++) { var hit = list.find(function (v) { return rank[i].test(v.name) && /^en/i.test(v.lang); }); if (hit) return hit; } return list.find(function (v) { return /en(-|_)US/i.test(v.lang); }) || list.find(function (v) { return /^en/i.test(v.lang); }) || null; } function cleanForSpeech(text) { return String(text || '') .replace(/```[\s\S]*?```/g, ' ') .replace(/[`*_#>\[\]()]/g, ' ') .replace(/\s+/g, ' ') .trim(); } function resetTts() { ttsBuf = ''; ttsActive = false; ttsChain = Promise.resolve(); if (window.speechSynthesis) window.speechSynthesis.cancel(); } function speakChunk(text) { var clean = cleanForSpeech(text); if (!clean || !window.speechSynthesis || settings.voiceSpeak === false) return Promise.resolve(); ttsActive = true; return loadVoices().then(function (voices) { return new Promise(function (resolve) { var u = new SpeechSynthesisUtterance(clean); speechSynthUtterance = u; u.rate = Number(settings.voiceRate) || 1.05; u.pitch = 1; var preferred = pickVoice(voices); if (preferred) u.voice = preferred; setVoiceUi('speaking'); var done = false; function finish() { if (done) return; done = true; resolve(); } u.onend = finish; u.onerror = finish; try { window.speechSynthesis.resume(); } catch (e) { /* ignore */ } window.speechSynthesis.speak(u); setTimeout(function () { if (!done && window.speechSynthesis.paused) { try { window.speechSynthesis.resume(); } catch (e) { /* ignore */ } } }, 200); }); }); } function enqueueSpeak(text) { if (!voiceMode) return; var clean = cleanForSpeech(text); if (!clean) return; ttsChain = ttsChain.then(function () { if (!voiceMode) return; return speakChunk(clean); }).catch(function () { /* keep queue alive */ }); } function feedSpeechDelta(delta) { if (!voiceMode || !delta) return; ttsBuf += delta; // Speak complete sentences as soon as they land (don't wait for full stream). var re = /([^.!?\n]+[.!?]+)(\s+|\n|$)/g; var m; var last = 0; var src = ttsBuf; while ((m = re.exec(src)) !== null) { enqueueSpeak(m[1]); last = re.lastIndex; } if (last > 0) ttsBuf = src.slice(last); // Long clause with no punctuation — don't leave user waiting forever if (ttsBuf.length > 160) { var cut = ttsBuf.lastIndexOf(' ', 140); if (cut > 48) { enqueueSpeak(ttsBuf.slice(0, cut)); ttsBuf = ttsBuf.slice(cut + 1); } } } async function flushSpeech() { if (ttsBuf.trim()) { enqueueSpeak(ttsBuf); ttsBuf = ''; } await ttsChain; ttsActive = false; if (voiceMode) setVoiceUi('listening'); } async function speakText(text) { resetTts(); enqueueSpeak(text); await flushSpeech(); } function stopVoiceMode() { voiceMode = false; voiceProcessing = false; if (voiceRecognition) { try { voiceRecognition.stop(); } catch (e) { /* ignore */ } voiceRecognition = null; } resetTts(); setVoiceUi('off'); } function startVoiceListen() { if (!voiceMode || streaming || voiceProcessing || ttsActive) return; var SR = getSpeechRecognition(); if (!SR) return; voiceRecognition = SR; voiceRecognition.continuous = false; voiceRecognition.interimResults = true; voiceRecognition.lang = 'en-US'; setVoiceUi('listening'); voiceRecognition.onresult = async function (e) { var transcript = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) transcript += e.results[i][0].transcript; } transcript = transcript.trim(); if (!transcript) return; voiceRecognition = null; if (!voiceMode) return; voiceProcessing = true; try { // sendMessage streams TTS while generating — no second full speak after await sendMessage(transcript, { fromVoice: true }); } finally { voiceProcessing = false; if (voiceMode) startVoiceListen(); } }; voiceRecognition.onerror = function (ev) { voiceRecognition = null; if (ev && (ev.error === 'aborted' || ev.error === 'no-speech')) { if (voiceMode && !streaming && !voiceProcessing) setTimeout(startVoiceListen, 250); return; } if (voiceMode && !streaming && !voiceProcessing) setTimeout(startVoiceListen, 500); }; voiceRecognition.onend = function () { if (voiceRecognition) voiceRecognition = null; if (voiceMode && !streaming && !voiceProcessing && !ttsActive) setTimeout(startVoiceListen, 200); }; try { voiceRecognition.start(); } catch (e) { /* ignore */ } } function toggleVoiceMode() { if (voiceMode) { stopVoiceMode(); return; } stopDictation(); var SR = getSpeechRecognition(); if (!SR) { alert('Voice mode requires speech recognition support (Chromium browsers work best).'); return; } if (!window.speechSynthesis) { alert('This browser cannot speak replies aloud.'); return; } voiceMode = true; unlockSpeech(); loadVoices(); setVoiceUi('listening'); startVoiceListen(); } async function sendMessage(text, opts = {}) { const attached = (opts.images || (opts.skipUserPush ? [] : pendingAttachments)).slice(); const contentText = String(text || '').trim(); if ((!contentText && !attached.length)) return null; // While streaming: enqueue text (Guide parity). Force/skip paths bypass. if (streaming && !opts.force && !opts.skipUserPush) { if (contentText) { addQueuedMessage(contentText); if (!opts.fromVoice) { els.input.value = ''; autoGrow(); } } return null; } ensureConversation(); const conv = getActive(); const chat = conv.messages; // Never re-send generated images (base64) into the model — that polluted // context to multi-MB and made "thanks" / next prompts regenerate old scenes. const historyForApi = chat.map(function (m) { const plain = messagePlainText(m.content); const userAtts = messageImagesForApi(m); if (m.role === 'assistant' && messageHasGeneratedImage(m)) { const note = plain ? plain + '\n[Image generated earlier in this chat.]' : '[Image generated earlier in this chat.]'; return { role: m.role, content: note }; } return { role: m.role, content: toApiContent(plain, userAtts) }; }); if (!opts.skipUserPush) { const apiUserContent = toApiContent(contentText, attached); historyForApi.push({ role: 'user', content: apiUserContent }); const nowTsUser = Date.now(); chat.push({ role: 'user', content: contentText || (attached.length ? 'Describe this image.' : ''), images: attached.map(function (a) { return { dataUrl: a.dataUrl }; }), createdAt: nowTsUser, }); pendingAttachments = []; renderAttachPreview(); } else if (!historyForApi.length || historyForApi[historyForApi.length - 1].role !== 'user') { historyForApi.push({ role: 'user', content: toApiContent(contentText, attached) }); } const nowTs = Date.now(); const assistant = { role: 'assistant', content: '', streaming: true, createdAt: nowTs }; chat.push(assistant); renderMessages(); if (!opts.fromVoice) { els.input.value = ''; autoGrow(); } streaming = true; autoGrow(); if (voiceMode) { resetTts(); } persistChat(); const headers = { 'Content-Type': 'application/json' }; if (authToken) headers.Authorization = `Bearer ${authToken}`; let replyText = null; activeAbort = new AbortController(); const signal = activeAbort.signal; try { const res = await fetch(apiUrl('/api/ai/proxy'), { method: 'POST', credentials: 'include', headers, signal, body: JSON.stringify({ provider: PROVIDER, model: MODEL_ID, systemPrompt: buildSystemPrompt(conv), messages: historyForApi, stream: settings.stream !== false, maxTokens: 4096, temperature: Math.min(1.5, Math.max(0, parseFloat(settings.temperature) || 0.7)), webSearch: settings.webSearch !== false, webFetch: settings.webFetch !== false, searchPrivacy: settings.searchPrivacy || 'privacy', searchProxy: (settings.searchProxy || '').trim().slice(0, 256), imageSize: settings.imageSize || '1024x1024', }), }); applyQuotaHeaders(res); lastRequestId = res.headers.get('X-Secrypt-Request-Id') || lastRequestId; if (res.status === 429) { const data = await res.json().catch(() => ({})); applyQuotaPayload(data); if (data.used == null && data.limit == null) { // headers already applied above; force exhausted display if body omitted counts if (quota.limit !== Infinity) quota.used = quota.limit; updateQuotaUI(); } chat.pop(); if (!opts.skipUserPush) chat.pop(); renderMessages(); persistChat(); openPaywall(data.message || 'Daily limit reached. Subscribe to continue.'); return null; } if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.message || data.error || `Request failed (${res.status})`); } if (settings.stream === false) { const data = await res.json().catch(function () { return {}; }); const text = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || data.content || ''; assistant.content = text; assistant.streaming = false; replyText = text; if (voiceMode && settings.voiceSpeak !== false) feedSpeechDelta(text); if (voiceMode) await flushSpeech(); renderMessages(); persistChat(); } else { const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let lastRender = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n'); buffer = parts.pop() || ''; let dirty = false; for (const line of parts) { const trimmed = line.trim(); if (!trimmed.startsWith('data:')) continue; const payload = trimmed.slice(5).trim(); if (payload === '[DONE]') continue; try { const json = JSON.parse(payload); if (json.type === 'status') { if (json.status === 'generating_image') { assistant.generatingImage = true; assistant.status = json.detail || 'Generating image…'; assistant.statusBoundary = 'private'; assistant.statusContinuing = false; } else if (json.status === 'continuing') { const n = json.continue || json.continueCount || 1; const max = json.maxContinues || 4; assistant.status = json.detail || ('Continuing long reply (' + n + '/' + max + ')…'); assistant.statusBoundary = 'private'; assistant.statusContinuing = true; recordTrustEvent({ kind: 'length_continue', n: n, max: max }); } else { assistant.status = json.detail || (json.status === 'searching' ? 'Searching the web…' : json.status === 'generating' ? 'Writing…' : 'Working…'); assistant.statusBoundary = json.boundary || (json.status === 'searching' ? 'external' : 'private'); assistant.statusContinuing = false; if (assistant.statusBoundary === 'external') { recordTrustEvent({ kind: 'boundary_external', tools: json.tools || [], privacyMode: json.privacyMode, proxyUsed: !!json.proxyUsed, }); } } dirty = true; continue; } if (json.type === 'image') { const mime = json.mimeType || 'image/png'; let dataUrl = ''; if (json.url) dataUrl = json.url; else if (json.b64) dataUrl = 'data:' + mime + ';base64,' + json.b64; if (dataUrl) { if (!Array.isArray(assistant.images)) assistant.images = []; assistant.images.push({ dataUrl: dataUrl, generated: true }); assistant.generatingImage = false; assistant.morphStyle = null; if (assistant.status) assistant.status = ''; dirty = true; } continue; } const delta = (json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content) || ''; if (delta) { if (assistant.status) assistant.status = ''; assistant.content += delta; dirty = true; if (voiceMode && settings.voiceSpeak !== false) feedSpeechDelta(delta); } } catch { /* partial */ } } if (dirty) { const now = Date.now(); if (now - lastRender > 40) { lastRender = now; renderMessages(); } } } assistant.status = ''; assistant.streaming = false; replyText = assistant.content; if (voiceMode) await flushSpeech(); renderMessages(); persistChat(); } } catch (err) { const aborted = !!(err && (err.name === 'AbortError' || (signal && signal.aborted))); assistant.streaming = false; // Keep the turn if we already showed progress (esp. long Flux gens). // Dropping user+assistant on network/SSE timeout made images vanish // even when the server finished successfully after Cloudflare cut idle streams. const keepTurn = aborted || !!assistant.content || !!assistant.generatingImage || !!(assistant.images && assistant.images.length) || !!assistant.status; if (!keepTurn) { chat.pop(); if (!opts.skipUserPush) chat.pop(); } else { assistant.generatingImage = false; assistant.morphStyle = null; if (aborted) { if (!assistant.content && !(assistant.images && assistant.images.length)) { assistant.content = ''; assistant.status = ''; chat.pop(); } } else if (!assistant.content && !(assistant.images && assistant.images.length)) { assistant.content = err.message || 'Connection interrupted. Try sending again.'; assistant.status = ''; } replyText = assistant.content; } renderMessages(); persistChat(); if (voiceMode && !aborted) stopVoiceMode(); if (!keepTurn && !aborted) { alert(err.message || 'Something went wrong'); } } finally { activeAbort = null; streaming = false; // Final in-try render often ran while streaming was still true, which hid // user message actions (Edit / Fork / checkpoint). Re-render once settled. renderMessages(); autoGrow(); if (!opts.fromVoice) els.input.focus(); scheduleQueueDrain(); } return replyText; } els.input.addEventListener('input', autoGrow); els.input.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; if (settings.enterSend !== false) { if (!e.shiftKey) { e.preventDefault(); els.form.requestSubmit(); } } else if (e.ctrlKey || e.metaKey) { e.preventDefault(); els.form.requestSubmit(); } }); document.querySelectorAll('.suggestion').forEach((btn) => { btn.addEventListener('click', () => { els.input.value = btn.getAttribute('data-prompt') || ''; autoGrow(); els.form.requestSubmit(); }); }); els.form.addEventListener('submit', async (e) => { e.preventDefault(); await sendMessage(els.input.value); }); els.newChatBtn.addEventListener('click', () => { startNewChat(); toggleSidebar(false); }); if (els.charactersBtn) els.charactersBtn.addEventListener('click', openCharactersModal); const openCharsFromSettings = document.getElementById('openCharactersFromSettings'); if (openCharsFromSettings) { openCharsFromSettings.addEventListener('click', function () { if (els.settingsModal) els.settingsModal.classList.remove('open'); openCharactersModal(); }); } const charCreateBtn = document.getElementById('charCreateBtn'); if (charCreateBtn) charCreateBtn.addEventListener('click', function () { showCharEditor(null); }); const charCloseTop = document.getElementById('charCloseTop'); if (charCloseTop) charCloseTop.addEventListener('click', function () { if (els.charactersModal) els.charactersModal.classList.remove('open'); }); const charEditSave = document.getElementById('charEditSave'); if (charEditSave) charEditSave.addEventListener('click', saveCharacterFromEditor); const charEditCancel = document.getElementById('charEditCancel'); if (charEditCancel) charEditCancel.addEventListener('click', hideCharEditor); const charEditDelete = document.getElementById('charEditDelete'); if (charEditDelete) charEditDelete.addEventListener('click', deleteCharacterFromEditor); if (els.charactersModal) { els.charactersModal.addEventListener('click', function (e) { if (e.target === els.charactersModal) els.charactersModal.classList.remove('open'); }); } els.themeBtn.addEventListener('click', () => { const cur = document.documentElement.getAttribute('data-theme') || 'dark'; const idx = THEME_PACKS.indexOf(cur); setTheme(THEME_PACKS[(idx + 1) % THEME_PACKS.length]); }); (function bindHistMenu() { const menu = document.getElementById('histMenu'); if (!menu) return; menu.querySelectorAll('[data-hist-act]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); const id = histMenuConvId; const act = btn.getAttribute('data-hist-act'); closeHistMenu(); if (!id) return; if (act === 'rename') renameConversation(id); if (act === 'share') exportConversation(id); if (act === 'archive') archiveConversation(id, true); if (act === 'unarchive') archiveConversation(id, false); if (act === 'delete') { deleteConversation(id); } }); }); document.addEventListener('click', function (e) { if (!menu.classList.contains('open')) return; if (e.target.closest('#histMenu') || e.target.closest('.history-item .more') || e.target.closest('#chatMenuBtn')) return; closeHistMenu(); }); })(); const chatMenuBtn = document.getElementById('chatMenuBtn'); if (chatMenuBtn) { chatMenuBtn.addEventListener('click', function (e) { e.stopPropagation(); const cur = getActive(); if (!cur) return; openHistMenu(cur.id, chatMenuBtn); }); } const imgLightbox = document.getElementById('imgLightbox'); const imgLightboxClose = document.getElementById('imgLightboxClose'); if (imgLightboxClose) imgLightboxClose.addEventListener('click', function () { if (imgLightbox) imgLightbox.classList.remove('open'); }); if (imgLightbox) { imgLightbox.addEventListener('click', function (e) { if (e.target === imgLightbox) imgLightbox.classList.remove('open'); }); } const regenGuest = document.getElementById('regenGuestAvatar'); if (regenGuest) regenGuest.addEventListener('click', regenerateGuestSeed); const openAcctSettings = document.getElementById('openAccountFromSettings'); if (openAcctSettings) { openAcctSettings.addEventListener('click', function () { if (els.settingsModal) els.settingsModal.classList.remove('open'); if (currentUser) openAccountModal(); else openAuthModal('signin'); }); } const delCurrent = document.getElementById('setDeleteCurrent'); if (delCurrent) { delCurrent.addEventListener('click', function () { const cur = getActive(); if (!cur) return; deleteConversation(cur.id); if (els.settingsModal) els.settingsModal.classList.remove('open'); }); } els.menuBtn.addEventListener('click', () => toggleSidebar(true)); els.sidebarOverlay.addEventListener('click', () => toggleSidebar(false)); els.micBtn.addEventListener('click', toggleDictation); els.voiceBtn.addEventListener('click', toggleVoiceMode); function setToggle(el, on) { if (!el) return; el.classList.toggle('on', !!on); el.setAttribute('aria-pressed', on ? 'true' : 'false'); } function readToggle(el) { return !!(el && el.classList.contains('on')); } function applySettingsToUi() { const sug = document.querySelector('.suggestions'); if (sug) sug.style.display = settings.suggestions ? '' : 'none'; if (els.attachBtn) els.attachBtn.style.display = settings.allowImages ? '' : 'none'; document.body.classList.toggle('atmosphere-on', settings.atmosphere !== false); document.body.classList.toggle('density-compact', settings.density === 'compact'); if (settings.themePack && THEME_PACKS.indexOf(settings.themePack) !== -1) { if (document.documentElement.getAttribute('data-theme') !== settings.themePack) { setTheme(settings.themePack); } } updateGuestIdentityUi(); try { if (settings.analytics) localStorage.removeItem('secrypt-analytics-optout'); else localStorage.setItem('secrypt-analytics-optout', '1'); } catch { /* */ } } function fillSettingsForm() { setToggle(document.getElementById('setAnalytics'), settings.analytics); setToggle(document.getElementById('setRememberSession'), settings.rememberSession); setToggle(document.getElementById('setShowEmail'), settings.showEmail); setToggle(document.getElementById('setGhostDefault'), settings.ghostDefault); setToggle(document.getElementById('setGhostDefaultGeneral'), settings.ghostDefault); setToggle(document.getElementById('setEncryptHistory'), settings.encryptHistory); setToggle(document.getElementById('setSaveHistory'), settings.saveHistory); setToggle(document.getElementById('setSaveImages'), settings.saveImages); setToggle(document.getElementById('setLinkAccount'), settings.linkAccount); setToggle(document.getElementById('setEnterSend'), settings.enterSend); setToggle(document.getElementById('setEnterSendGeneral'), settings.enterSend); setToggle(document.getElementById('setSuggestions'), settings.suggestions); setToggle(document.getElementById('setStream'), settings.stream); setToggle(document.getElementById('setVoiceSpeak'), settings.voiceSpeak); setToggle(document.getElementById('setVoiceAutoListen'), settings.voiceAutoListen); setToggle(document.getElementById('setWebSearch'), settings.webSearch); setToggle(document.getElementById('setWebFetch'), settings.webFetch); const sp = document.getElementById('setSearchPrivacy'); if (sp) sp.value = settings.searchPrivacy || 'privacy'; const sproxy = document.getElementById('setSearchProxy'); if (sproxy) sproxy.value = settings.searchProxy || ''; setToggle(document.getElementById('setAllowImages'), settings.allowImages); const imgSz = document.getElementById('setImageSize'); if (imgSz) imgSz.value = settings.imageSize || '1024x1024'; const proxyHint = document.getElementById('searchProxyHint'); if (proxyHint) { const v = validateSearchProxy(settings.searchProxy || ''); proxyHint.textContent = v.ok ? ((settings.searchProxy || '').trim() ? 'Proxy OK — search/fetch will use it.' : '') : (v.error || ''); } setToggle(document.getElementById('setShowTimestamps'), settings.showTimestamps); setToggle(document.getElementById('setShowTimestampsChat'), settings.showTimestamps); setToggle(document.getElementById('setAtmosphere'), settings.atmosphere !== false); const rl = document.getElementById('setReplyLength'); if (rl) rl.value = settings.replyLength || 'balanced'; const temp = document.getElementById('setTemperature'); if (temp) temp.value = String(settings.temperature || '0.7'); const persona = document.getElementById('setPersona'); if (persona) persona.value = settings.persona || ''; const dn = document.getElementById('setDisplayName'); if (dn) dn.value = settings.displayName || ''; const dng = document.getElementById('setDisplayNameGeneral'); if (dng) dng.value = settings.displayName || ''; const vr = document.getElementById('setVoiceRate'); if (vr) vr.value = String(settings.voiceRate || '1.05'); const dens = document.getElementById('setDensity'); if (dens) dens.value = settings.density || 'comfortable'; const themeSel = document.getElementById('setThemePack'); if (themeSel) themeSel.value = settings.themePack || localStorage.getItem(THEME_KEY) || 'dark'; } function collectSettingsForm() { const enterEl = document.getElementById('setEnterSendGeneral') || document.getElementById('setEnterSend'); const ghostEl = document.getElementById('setGhostDefaultGeneral') || document.getElementById('setGhostDefault'); const tsEl = document.getElementById('setShowTimestamps') || document.getElementById('setShowTimestampsChat'); const nameGeneral = ((document.getElementById('setDisplayNameGeneral') || {}).value || '').trim(); const namePersona = ((document.getElementById('setDisplayName') || {}).value || '').trim(); return { analytics: readToggle(document.getElementById('setAnalytics')), rememberSession: readToggle(document.getElementById('setRememberSession')), showEmail: readToggle(document.getElementById('setShowEmail')), ghostDefault: readToggle(ghostEl), encryptHistory: readToggle(document.getElementById('setEncryptHistory')), saveHistory: readToggle(document.getElementById('setSaveHistory')), saveImages: readToggle(document.getElementById('setSaveImages')), linkAccount: readToggle(document.getElementById('setLinkAccount')), enterSend: readToggle(enterEl), suggestions: readToggle(document.getElementById('setSuggestions')), stream: readToggle(document.getElementById('setStream')), replyLength: (document.getElementById('setReplyLength') || {}).value || 'balanced', temperature: (document.getElementById('setTemperature') || {}).value || '0.7', persona: ((document.getElementById('setPersona') || {}).value || '').slice(0, 2000), displayName: (nameGeneral || namePersona).slice(0, 40), voiceSpeak: readToggle(document.getElementById('setVoiceSpeak')), voiceAutoListen: readToggle(document.getElementById('setVoiceAutoListen')), voiceRate: (document.getElementById('setVoiceRate') || {}).value || '1.05', webSearch: readToggle(document.getElementById('setWebSearch')), searchPrivacy: (document.getElementById('setSearchPrivacy') || {}).value || 'privacy', searchProxy: ((document.getElementById('setSearchProxy') || {}).value || '').trim().slice(0, 256), webFetch: readToggle(document.getElementById('setWebFetch')), allowImages: readToggle(document.getElementById('setAllowImages')), imageSize: (document.getElementById('setImageSize') || {}).value || '1024x1024', showTimestamps: readToggle(tsEl), atmosphere: readToggle(document.getElementById('setAtmosphere')), density: (document.getElementById('setDensity') || {}).value || 'comfortable', themePack: (document.getElementById('setThemePack') || {}).value || 'dark', }; } function openSettings() { fillSettingsForm(); closeAllModals(); if (els.settingsModal) els.settingsModal.classList.add('open'); toggleSidebar(false); } document.querySelectorAll('#settingsTabs button').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('#settingsTabs button').forEach(function (b) { b.classList.remove('active'); }); document.querySelectorAll('.settings-pane').forEach(function (pane) { pane.classList.remove('active'); }); btn.classList.add('active'); const pane = document.getElementById('pane-' + btn.getAttribute('data-pane')); if (pane) pane.classList.add('active'); }); }); document.querySelectorAll('#settingsModal .toggle').forEach(function (btn) { btn.addEventListener('click', function () { btn.classList.toggle('on'); btn.setAttribute('aria-pressed', btn.classList.contains('on') ? 'true' : 'false'); }); }); if (els.settingsBtn) els.settingsBtn.addEventListener('click', openSettings); if (els.settingsClose) els.settingsClose.addEventListener('click', function () { if (els.settingsModal) els.settingsModal.classList.remove('open'); }); if (els.settingsSave) els.settingsSave.addEventListener('click', function () { const next = collectSettingsForm(); const proxyCheck = validateSearchProxy(next.searchProxy); const hint = document.getElementById('searchProxyHint'); if (!proxyCheck.ok) { if (hint) hint.textContent = proxyCheck.error; window.alert(proxyCheck.error || 'Fix search proxy before saving.'); return; } next.searchProxy = proxyCheck.value; if (hint) { hint.textContent = next.searchProxy ? 'Proxy OK — search/fetch will use it.' : (next.searchPrivacy === 'all' ? 'Standard search may use Google/Bing — queries leave Secrypt.' : ''); } const wasEncrypt = !!settings.encryptHistory; saveSettingsObj(next); setTheme(next.themePack || 'dark'); if (next.ghostDefault && !getActive()) ghostSession = true; updateGhostUi(); if (next.encryptHistory !== wasEncrypt || next.saveHistory) saveConversations(); renderMessages(); if (els.settingsModal) els.settingsModal.classList.remove('open'); }); // Keep mirrored toggles in sync while editing [['setEnterSend', 'setEnterSendGeneral'], ['setGhostDefault', 'setGhostDefaultGeneral'], ['setShowTimestamps', 'setShowTimestampsChat']].forEach(function (pair) { pair.forEach(function (id, idx) { const el = document.getElementById(id); if (!el) return; el.addEventListener('click', function () { const other = document.getElementById(pair[1 - idx]); if (other) { setTimeout(function () { setToggle(other, el.classList.contains('on')); }, 0); } }); }); }); const dnPersona = document.getElementById('setDisplayName'); const dnGeneral = document.getElementById('setDisplayNameGeneral'); if (dnPersona && dnGeneral) { dnPersona.addEventListener('input', function () { dnGeneral.value = dnPersona.value; }); dnGeneral.addEventListener('input', function () { dnPersona.value = dnGeneral.value; }); } function bindGhostToggle(el) { if (!el) return; el.addEventListener('click', function () { if (streaming) return; const turningOn = !ghostSession; if (turningOn) { // start a fresh ghost thread const prev = getActive(); if (prev && prev.messages && prev.messages.length && !prev.ghost) { // leave saved chat; open new ghost activeId = null; } setGhostMode(true, { startFresh: true }); ensureConversation(); renderHistory(); renderMessages(); } else { setGhostMode(false); // if current ghost had messages, keep in memory but mark saveable const cur = getActive(); if (cur) { cur.ghost = false; if (!cur.title || cur.title === 'Ghost chat') cur.title = titleFromMessage((cur.messages[0] && messagePlainText(cur.messages[0].content)) || 'New chat'); } saveConversations(); renderHistory(); renderMessages(); } toggleSidebar(false); }); } bindGhostToggle(document.getElementById('ghostToggle')); bindGhostToggle(document.getElementById('ghostToggleSide')); if (els.settingsModal) { els.settingsModal.addEventListener('click', function (e) { if (e.target === els.settingsModal) els.settingsModal.classList.remove('open'); }); } const exportBtn = document.getElementById('setExportData'); if (exportBtn) exportBtn.addEventListener('click', function () { const blob = new Blob([JSON.stringify({ exportedAt: new Date().toISOString(), conversations: conversations }, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'secrypt-conversations.json'; a.click(); URL.revokeObjectURL(a.href); }); const trustReceiptBtn = document.getElementById('setTrustReceipt'); if (trustReceiptBtn) trustReceiptBtn.addEventListener('click', downloadTrustReceipt); const clearBtn = document.getElementById('setClearHistory'); if (clearBtn) clearBtn.addEventListener('click', function () { if (!confirm('Delete all local conversation history on this device?')) return; conversations = []; activeId = null; try { localStorage.removeItem(STORAGE_KEY); } catch { /* */ } startNewChat(); renderHistory(); renderMessages(); }); const exportEncBtn = document.getElementById('setExportEnc'); if (exportEncBtn) exportEncBtn.addEventListener('click', async function () { const pass = prompt('Choose a passphrase for this encrypted backup (you will need it to restore):'); if (!pass || pass.length < 8) { alert('Passphrase must be at least 8 characters.'); return; } try { const { key, saltB64 } = await derivePassphraseKey(pass); const envelope = await encryptJsonPayload( { exportedAt: new Date().toISOString(), conversations: persistableConversations() }, key ); const blob = new Blob( [JSON.stringify({ secryptBackup: 1, kdf: 'PBKDF2-SHA256-120000', salt: saltB64, ...envelope }, null, 2)], { type: 'application/json' } ); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'secrypt-conversations.encrypted.json'; a.click(); URL.revokeObjectURL(a.href); } catch (err) { alert('Encrypted export failed: ' + (err.message || err)); } }); const importEncBtn = document.getElementById('setImportEnc'); const importEncFile = document.getElementById('setImportEncFile'); if (importEncBtn && importEncFile) { importEncBtn.addEventListener('click', function () { importEncFile.click(); }); importEncFile.addEventListener('change', async function () { const file = importEncFile.files && importEncFile.files[0]; importEncFile.value = ''; if (!file) return; const pass = prompt('Enter the passphrase for this encrypted backup:'); if (!pass) return; try { const parsed = JSON.parse(await file.text()); if (!parsed || !parsed.ct || !parsed.salt) throw new Error('Not a Secrypt encrypted backup'); const { key } = await derivePassphraseKey(pass, parsed.salt); const data = await decryptJsonPayload(parsed, key); const list = Array.isArray(data.conversations) ? data.conversations : (Array.isArray(data) ? data : null); if (!list) throw new Error('Backup had no conversations'); if (!confirm('Restore ' + list.length + ' conversations? This replaces local history on this device.')) return; conversations = list.map((c) => Object.assign({}, c, { ghost: false })); activeId = conversations[0] ? conversations[0].id : null; settings.encryptHistory = true; saveSettingsObj(Object.assign({}, settings, { encryptHistory: true })); saveConversations(); renderHistory(); renderMessages(); alert('Restore complete.'); } catch (err) { alert('Restore failed: ' + (err.message || 'wrong passphrase or corrupt file')); } }); } applySettingsToUi(); updateGhostUi(); renderCharPins(); function pickHero() { const h = HEROES[Math.floor(Math.random() * HEROES.length)]; if (els.heroTitle) els.heroTitle.textContent = h.title; if (els.heroSub) els.heroSub.textContent = h.sub; document.title = 'Secrypt — ' + h.title; } pickHero(); function renderAttachPreview() { if (!els.attachPreview) return; els.attachPreview.innerHTML = ''; els.attachPreview.classList.toggle('has-items', pendingAttachments.length > 0); pendingAttachments.forEach(function (att, idx) { const chip = document.createElement('div'); chip.className = 'attach-chip'; chip.innerHTML = ''; chip.querySelector('button').addEventListener('click', function () { pendingAttachments.splice(idx, 1); renderAttachPreview(); autoGrow(); }); els.attachPreview.appendChild(chip); }); autoGrow(); } function fileToDataUrl(file) { return new Promise(function (resolve, reject) { if (!file || !file.type.startsWith('image/')) { reject(new Error('Only images are supported right now')); return; } if (file.size > 8 * 1024 * 1024) { reject(new Error('Image too large (max 8MB)')); return; } const img = new Image(); const url = URL.createObjectURL(file); img.onload = function () { const max = 1280; let w = img.naturalWidth; let h = img.naturalHeight; if (w > max || h > max) { const scale = Math.min(max / w, max / h); w = Math.round(w * scale); h = Math.round(h * scale); } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); URL.revokeObjectURL(url); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); resolve({ name: file.name, dataUrl: dataUrl, mime: 'image/jpeg' }); }; img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Could not read image')); }; img.src = url; }); } async function addFiles(fileList) { if (!settings.allowImages) return; const files = Array.from(fileList || []); for (const f of files) { if (pendingAttachments.length >= 4) break; try { const att = await fileToDataUrl(f); pendingAttachments.push(att); } catch (err) { alert(err.message || 'Could not attach file'); } } renderAttachPreview(); } function toApiContent(text, images) { const imgs = images || []; if (!imgs.length) return text; const parts = []; imgs.forEach(function (att) { parts.push({ type: 'image_url', image_url: { url: att.dataUrl } }); }); if (text && String(text).trim()) { parts.push({ type: 'text', text: String(text) }); } else { parts.push({ type: 'text', text: 'Describe this image.' }); } return parts; } function messagePlainText(content) { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content.filter(function (p) { return p && p.type === 'text'; }).map(function (p) { return p.text; }).join(' ').trim(); } return ''; } function messageImages(m) { if (m && Array.isArray(m.images)) return m.images; if (Array.isArray(m && m.content)) { return m.content.filter(function (p) { return p && p.type === 'image_url'; }).map(function (p) { return { dataUrl: p.image_url && p.image_url.url }; }).filter(function (x) { return x.dataUrl; }); } return []; } function messageHasGeneratedImage(m) { return messageImages(m).some(function (a) { return a && a.generated; }); } /** User-uploaded attachments only — never generated Flux pixels for the API. */ function messageImagesForApi(m) { if (!m || m.role !== 'user') return []; return messageImages(m).filter(function (a) { return a && a.dataUrl && !a.generated; }); } /** Soft noise morph while waiting — one locked palette per generation (no mid-gen recolor). */ const _morphRafs = new WeakMap(); const MORPH_PALETTES = [ { r: 30, g: 40, b: 70, rs: 0.35, gs: 0.45, bs: 0.55 }, // teal deep { r: 50, g: 20, b: 55, rs: 0.45, gs: 0.25, bs: 0.55 }, // violet { r: 20, g: 45, b: 40, rs: 0.3, gs: 0.5, bs: 0.4 }, // forest { r: 55, g: 30, b: 25, rs: 0.5, gs: 0.35, bs: 0.25 }, // ember { r: 25, g: 35, b: 55, rs: 0.25, gs: 0.4, bs: 0.6 }, // cobalt { r: 40, g: 25, b: 45, rs: 0.4, gs: 0.3, bs: 0.5 }, // plum ]; function pickMorphStyle() { const pal = MORPH_PALETTES[Math.floor(Math.random() * MORPH_PALETTES.length)]; return { pal, fx: 0.04 + Math.random() * 0.06, fy: 0.03 + Math.random() * 0.05, fz: 0.02 + Math.random() * 0.04, sp1: 1.2 + Math.random() * 1.2, sp2: 0.9 + Math.random() * 1.0, sp3: 1.5 + Math.random() * 1.4, }; } function startImageMorph(canvas, style) { if (!canvas || !canvas.getContext) return; if (_morphRafs.has(canvas)) return; const ctx = canvas.getContext('2d'); const w = canvas.width; const h = canvas.height; let t0 = performance.now(); const s = style || pickMorphStyle(); const pal = s.pal; const fx = s.fx; const fy = s.fy; const fz = s.fz; const sp1 = s.sp1; const sp2 = s.sp2; const sp3 = s.sp3; function frame(now) { if (!canvas.isConnected) { _morphRafs.delete(canvas); return; } const t = (now - t0) / 1000; const img = ctx.createImageData(w, h); const d = img.data; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; const n = Math.sin(x * fx + t * sp1) * Math.cos(y * fy - t * sp2) + Math.sin((x + y) * fz + t * sp3); const v = (n * 0.5 + 0.5) * 255; d[i] = pal.r + v * pal.rs; d[i + 1] = pal.g + v * pal.gs; d[i + 2] = pal.b + v * pal.bs; d[i + 3] = 255; } } ctx.putImageData(img, 0, 0); const id = requestAnimationFrame(frame); _morphRafs.set(canvas, id); } const id = requestAnimationFrame(frame); _morphRafs.set(canvas, id); } if (els.attachBtn && els.fileInput) { els.attachBtn.addEventListener('click', function () { els.fileInput.click(); }); els.fileInput.addEventListener('change', function () { addFiles(els.fileInput.files); els.fileInput.value = ''; }); } ;['dragenter', 'dragover'].forEach(function (ev) { els.composer.addEventListener(ev, function (e) { e.preventDefault(); e.stopPropagation(); els.composer.classList.add('drag-over'); }); }); ;['dragleave', 'drop'].forEach(function (ev) { els.composer.addEventListener(ev, function (e) { e.preventDefault(); e.stopPropagation(); if (ev === 'dragleave' && e.target !== els.composer) return; els.composer.classList.remove('drag-over'); }); }); els.composer.addEventListener('drop', function (e) { const files = e.dataTransfer && e.dataTransfer.files; if (files && files.length) addFiles(files); }); // Also allow drop on main chat area els.messagesWrap.addEventListener('dragover', function (e) { e.preventDefault(); }); els.messagesWrap.addEventListener('drop', function (e) { e.preventDefault(); if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files); }); loadConversations().then(function () { if (conversations.length) { activeId = [...conversations].filter(function (c) { return !c.ghost && !c.archived; }).sort(function (a, b) { return b.updatedAt - a.updatedAt; })[0]?.id || null; } ghostSession = !!settings.ghostDefault; if (!quota.resetAt) quota.resetAt = nextUtcMidnightIso(); updateGhostUi(); paintGuestAvatars(); renderHistory(); renderMessages(); (function(){function syncSeo(){var empty=document.getElementById('emptyState');var active=!empty||empty.style.display==='none'||empty.hidden;document.body.classList.toggle('chat-active',!!active);}var _orig=renderMessages;renderMessages=function(){var r=_orig.apply(null,arguments);try{syncSeo();}catch(e){}return r;};syncSeo();})(); updateQuotaUI(); autoGrow(); refreshAuth().then(handleBootParams); });