📜
settings.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// Global namespace to share state across modules window.App = window.App || {}; (() => { const els = { // dynamically injected container settingsOverlay: document.getElementById('settingsOverlay'), overlayBackdrop: document.getElementById('overlayBackdrop'), openSettings: document.getElementById('openSettings'), closeSettings: document.getElementById('closeSettings'), settingsBody: document.getElementById('settingsBody'), clearChat: null, }; const STORAGE_KEY = 'unified-chat-state-v2'; // Pricing per million tokens (input / output) const MODEL_PRICING = { 'deepseek-chat': { input: 0.27, output: 1.10, inputCache: 0.07 }, 'deepseek-reasoner': { input: 0.55, output: 2.19, inputCache: 0.14 }, 'gpt-4o': { input: 2.50, output: 10.00 }, 'gpt-4o-mini': { input: 0.15, output: 0.60 }, 'gpt-5': { input: 1.25, output: 10.00 }, 'gpt-5-mini': { input: 0.25, output: 2.00 }, 'gpt-5-nano': { input: 0.05, output: 0.40 }, 'gpt-5-thinking': { input: 1.25, output: 10.00 }, 'gpt-5-pro': { input: 2.50, output: 15.00 }, 'grok-3': { input: 3.00, output: 15.00 }, 'grok-3-mini': { input: 0.30, output: 0.50 }, 'grok-code-fast-1': { input: 0.20, output: 1.50 }, 'grok-4-0709': { input: 3.00, output: 15.00 } }; const defaultState = () => ({ settings: { model: 'deepseek-chat', maxTokens: 800, temperature: 0.7, forceTemperature: false, includeArtifacts: false, jsonFormat: false, system: 'You are a helpful, accurate assistant. Be concise and clear. Use markdown when it helps readability.', codeStyle: 'default', fullCodePrompt: 'When providing code examples, always include complete, runnable code with all necessary imports, setup, and context. Provide full file contents rather than partial snippets.', snippetsPrompt: 'Focus on providing concise code snippets that show only the relevant changes or key parts. Explain what each snippet does and where it should be used.', chunkedPrompt: 'When providing large code files or long responses, break them into logical chunks. End each chunk with a clear indication of what comes next. If approaching token limits, stop at a logical break point and indicate there\'s more to follow.' }, messages: [], stitcher: { chunks: [], isOpen: false }, stats: { totalMessages: 0, modelUsage: {} // { 'model-name': { input: 0, output: 0, calls: 0 } } } }); function loadState() { try { const state = JSON.parse(localStorage.getItem(STORAGE_KEY)) || defaultState(); // Ensure stats exists if (!state.stats) state.stats = { totalMessages: 0, modelUsage: {} }; return state; } catch { return defaultState(); } } function saveState(s) { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } App.state = loadState(); App.saveState = saveState; App.defaultState = defaultState; // Track token usage function trackUsage(model, usage) { if (!usage) return; const stats = App.state.stats; if (!stats.modelUsage[model]) { stats.modelUsage[model] = { input: 0, output: 0, calls: 0 }; } stats.modelUsage[model].input += usage.prompt_tokens || 0; stats.modelUsage[model].output += usage.completion_tokens || 0; stats.modelUsage[model].calls += 1; stats.totalMessages += 1; saveState(App.state); } App.trackUsage = trackUsage; // Calculate costs function calculateCosts() { const stats = App.state.stats; let totalCost = 0; const breakdown = []; Object.keys(stats.modelUsage).forEach(model => { const usage = stats.modelUsage[model]; const pricing = MODEL_PRICING[model]; if (!pricing) { breakdown.push({ model, input: usage.input, output: usage.output, calls: usage.calls, cost: 0, note: 'Pricing unknown' }); return; } // Calculate cost (tokens / 1M * price) const inputCost = (usage.input / 1000000) * pricing.input; const outputCost = (usage.output / 1000000) * pricing.output; const cost = inputCost + outputCost; totalCost += cost; breakdown.push({ model, input: usage.input, output: usage.output, calls: usage.calls, cost, inputCost, outputCost }); }); return { totalCost, breakdown }; } function formatNumber(num) { return num.toLocaleString(); } function formatCost(cost) { if (cost < 0.01) return `$${cost.toFixed(4)}`; if (cost < 1) return `$${cost.toFixed(3)}`; return `$${cost.toFixed(2)}`; } function renderStats() { const { totalCost, breakdown } = calculateCosts(); const stats = App.state.stats; if (breakdown.length === 0) { return `<div class="text-center text-zinc-500 text-sm py-4">No usage data yet</div>`; } let html = ` <div class="bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30 rounded-lg p-4 border border-indigo-200 dark:border-indigo-800"> <div class="flex items-center justify-between mb-2"> <h4 class="font-semibold text-sm">Conversation Statistics</h4> <button id="resetStats" class="text-xs px-2 py-1 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-800">Reset</button> </div> <div class="grid grid-cols-2 gap-3 mb-3"> <div class="bg-white dark:bg-zinc-900 rounded p-2"> <div class="text-xs text-zinc-500">Total Messages</div> <div class="text-lg font-bold">${stats.totalMessages}</div> </div> <div class="bg-white dark:bg-zinc-900 rounded p-2"> <div class="text-xs text-zinc-500">Estimated Cost</div> <div class="text-lg font-bold text-indigo-600 dark:text-indigo-400">${formatCost(totalCost)}</div> </div> </div> <div class="space-y-2"> `; breakdown.forEach(item => { const totalTokens = item.input + item.output; html += ` <div class="bg-white dark:bg-zinc-900 rounded p-3 text-xs"> <div class="flex items-center justify-between mb-1"> <span class="font-semibold">${item.model}</span> <span class="font-bold text-indigo-600 dark:text-indigo-400">${formatCost(item.cost)}</span> </div> <div class="grid grid-cols-3 gap-2 text-zinc-600 dark:text-zinc-400"> <div> <div class="text-[10px] uppercase text-zinc-400">Input</div> <div>${formatNumber(item.input)}</div> </div> <div> <div class="text-[10px] uppercase text-zinc-400">Output</div> <div>${formatNumber(item.output)}</div> </div> <div> <div class="text-[10px] uppercase text-zinc-400">Calls</div> <div>${item.calls}</div> </div> </div> ${item.note ? `<div class="mt-1 text-[10px] text-amber-600 dark:text-amber-400">${item.note}</div>` : ''} </div> `; }); html += ` </div> <div class="mt-3 text-[10px] text-zinc-500"> * Costs are estimates based on standard pricing. Cache hits, promotions, and other factors may affect actual costs. </div> </div> `; return html; } // Render the settings body (so HTML is slimmer in index.html) function renderSettingsBody() { els.settingsBody.innerHTML = ` ${renderStats()} <div class="mt-4"> <label class="text-sm block">Model</label> <select id="model" class="w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm"> <optgroup label="OpenAI"> <option>gpt-5</option> <option>gpt-5-mini</option> <option>gpt-5-nano</option> <option>gpt-5-thinking</option> <option>gpt-5-pro</option> <option>gpt-4o</option> <option>gpt-4o-mini</option> </optgroup> <optgroup label="DeepSeek"> <option selected>deepseek-chat</option> <option>deepseek-reasoner</option> </optgroup> <optgroup label="xAI (Grok)"> <option>grok-3</option> <option>grok-3-mini</option> <option>grok-code-fast-1</option> <option>grok-4-0709</option> </optgroup> </select> </div> <div class="mt-3"> <label class="text-sm block">Max tokens: <span id="maxTokensVal" class="font-mono">${App.state.settings.maxTokens}</span></label> <input id="maxTokens" type="range" min="64" max="4096" step="32" value="${App.state.settings.maxTokens}" class="w-full"> </div> <div class="mt-3"> <label class="text-sm block">Temperature: <span id="tempVal" class="font-mono">${App.state.settings.temperature}</span></label> <input id="temperature" type="range" min="0" max="2" step="0.1" value="${App.state.settings.temperature}" class="w-full"> <label class="flex items-center gap-2 mt-1 text-xs"><input id="forceTemperature" type="checkbox" class="accent-indigo-600" ${App.state.settings.forceTemperature ? 'checked' : ''}> Force temperature (for GPT-5)</label> </div> <div class="mt-3"> <label class="text-sm block mb-1">System prompt</label> <textarea id="system" rows="3" class="w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm" placeholder="You are a helpful, accurate assistant. Be concise and clear. Use markdown when it helps readability.">${App.state.settings.system || ''}</textarea> </div> <div class="mt-3"> <label class="text-sm block mb-2">Code Response Style</label> <div class="flex sm:flex-row flex-col mobile-tabs border border-zinc-300 dark:border-zinc-700 rounded-lg overflow-hidden"> <button id="tabDefault" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs">Default</button> <button id="tabFullCode" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs">Full Code</button> <button id="tabSnippets" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs">Snippets</button> <button id="tabChunked" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs">Chunked</button> </div> <div id="promptDefault" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Default:</strong> Normal responses with code in markdown blocks when helpful. </div> <div id="promptFullCode" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Full Code:</strong> Always provide complete, working code examples. <textarea id="fullCodePrompt" rows="3" class="w-full mt-2 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs">${App.state.settings.fullCodePrompt || ''}</textarea> </div> <div id="promptSnippets" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Snippets:</strong> Focus on concise code snippets and key changes only. <textarea id="snippetsPrompt" rows="3" class="w-full mt-2 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs">${App.state.settings.snippetsPrompt || ''}</textarea> </div> <div id="promptChunked" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Chunked:</strong> Break code into manageable chunks to fit token limits. <textarea id="chunkedPrompt" rows="3" class="w-full mt-2 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs">${App.state.settings.chunkedPrompt || ''}</textarea> </div> </div> <div class="mt-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2"> <label class="flex items-center gap-2 text-sm"><input id="includeArtifacts" type="checkbox" class="accent-indigo-600" ${App.state.settings.includeArtifacts ? 'checked' : ''}> Include artifacts from session</label> <label class="flex items-center gap-2 text-sm"><input id="jsonFormat" type="checkbox" class="accent-indigo-600" ${App.state.settings.jsonFormat ? 'checked' : ''}> Response JSON</label> </div> <div class="mt-3 flex items-center justify-end gap-2"> <button id="clearChat" class="text-xs px-3 py-1.5 rounded-md border border-red-300 dark:border-red-700 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900">Clear Chat</button> </div> <details class="mt-2 text-xs text-zinc-500"> <summary class="cursor-pointer mb-1">Session & CORS notes</summary> <p class="mt-1">Serve this file and <code>api.php</code> from the same origin to keep PHP session history. If cross-origin, enable credentials and set a specific <code>Access-Control-Allow-Origin</code> instead of <code>*</code>.</p> </details> `; } App.renderSettings = renderSettingsBody; function bindAndHydrate() { const s = App.state.settings; const $ = id => document.getElementById(id); const model = $('model'); const maxTokens = $('maxTokens'); const maxTokensVal = $('maxTokensVal'); const temperature = $('temperature'); const tempVal = $('tempVal'); const forceTemperature = $('forceTemperature'); const includeArtifacts = $('includeArtifacts'); const jsonFormat = $('jsonFormat'); const system = $('system'); const fullCodePrompt = $('fullCodePrompt'); const snippetsPrompt = $('snippetsPrompt'); const chunkedPrompt = $('chunkedPrompt'); const resetStats = $('resetStats'); const clearChat = $('clearChat'); const tabDefault = $('tabDefault'); const tabFullCode = $('tabFullCode'); const tabSnippets = $('tabSnippets'); const tabChunked = $('tabChunked'); const promptDefault = $('promptDefault'); const promptFullCode = $('promptFullCode'); const promptSnippets = $('promptSnippets'); const promptChunked = $('promptChunked'); // Active tab function setActiveTab(tabName) { const allTabs = [tabDefault, tabFullCode, tabSnippets, tabChunked]; const panes = { default: promptDefault, fullCode: promptFullCode, snippets: promptSnippets, chunked: promptChunked }; allTabs.forEach(b => b.className = 'mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0'); Object.values(panes).forEach(p => p.classList.add('hidden')); const map = { default: tabDefault, fullCode: tabFullCode, snippets: tabSnippets, chunked: tabChunked }; map[tabName].className = 'mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-indigo-600 text-white border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0'; panes[tabName].classList.remove('hidden'); s.codeStyle = tabName; App.saveState(App.state); } setActiveTab(s.codeStyle || 'default'); // Handlers maxTokens.addEventListener('input', () => { maxTokensVal.textContent = maxTokens.value; s.maxTokens = +maxTokens.value; App.saveState(App.state); }); temperature.addEventListener('input', () => { tempVal.textContent = temperature.value; s.temperature = +temperature.value; App.saveState(App.state); }); forceTemperature.addEventListener('change', () => { s.forceTemperature = forceTemperature.checked; App.saveState(App.state); }); includeArtifacts.addEventListener('change', () => { s.includeArtifacts = includeArtifacts.checked; App.saveState(App.state); }); jsonFormat.addEventListener('change', () => { s.jsonFormat = jsonFormat.checked; App.saveState(App.state); }); model.addEventListener('change', () => { s.model = model.value; App.saveState(App.state); }); system.addEventListener('input', () => { s.system = system.value; App.saveState(App.state); }); fullCodePrompt.addEventListener('input', () => { s.fullCodePrompt = fullCodePrompt.value; App.saveState(App.state); }); snippetsPrompt.addEventListener('input', () => { s.snippetsPrompt = snippetsPrompt.value; App.saveState(App.state); }); chunkedPrompt.addEventListener('input', () => { s.chunkedPrompt = chunkedPrompt.value; App.saveState(App.state); }); tabDefault.addEventListener('click', () => setActiveTab('default')); tabFullCode.addEventListener('click', () => setActiveTab('fullCode')); tabSnippets.addEventListener('click', () => setActiveTab('snippets')); tabChunked.addEventListener('click', () => setActiveTab('chunked')); // Reset stats if (resetStats) { resetStats.addEventListener('click', () => { if (confirm('Reset all usage statistics? This cannot be undone.')) { App.state.stats = { totalMessages: 0, modelUsage: {} }; App.saveState(App.state); renderSettingsBody(); bindAndHydrate(); } }); } // Clear chat if (clearChat) { clearChat.addEventListener('click', () => { if (confirm('Clear all chat messages? This cannot be undone.')) { App.state.messages = []; App.saveState(App.state); if (App.renderTranscript) App.renderTranscript(); const status = document.getElementById('status'); if (status) { status.textContent = 'Chat cleared'; setTimeout(() => status.textContent = '', 1500); } closeSettings(); } }); } } // Open/close overlay function openSettings() { els.settingsOverlay.classList.remove('hidden'); renderSettingsBody(); bindAndHydrate(); setTimeout(() => { try { document.getElementById('model').focus(); } catch {} }, 0); } function closeSettings() { els.settingsOverlay.classList.add('hidden'); } // Expose for others App.openSettings = openSettings; App.closeSettings = closeSettings; // Wire chrome els.openSettings.addEventListener('click', openSettings); els.closeSettings.addEventListener('click', closeSettings); els.overlayBackdrop.addEventListener('click', closeSettings); // Small viewport helper function handleViewportChange() { const q = document.getElementById('question'); if (!q) return; if (window.innerWidth < 640) { q.rows = 2; document.body.style.maxWidth = '100vw'; } else { q.rows = 3; document.body.style.maxWidth = ''; } } window.addEventListener('resize', handleViewportChange); handleViewportChange(); })();