// 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';
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 }
});
function loadState() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || defaultState(); }
catch { return defaultState(); }
}
function saveState(s) { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); }
App.state = loadState();
App.saveState = saveState;
App.defaultState = defaultState;
// Render the settings body (so HTML is slimmer in index.html)
function renderSettingsBody() {
els.settingsBody.innerHTML = `
<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 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-2 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800">Clear</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>
`;
}
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 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'));
els.clearChat = $('clearChat');
els.clearChat.addEventListener('click', () => {
App.state.messages = []; App.saveState(App.state);
if (App.renderTranscript) App.renderTranscript();
const status = document.getElementById('status');
if (status) { status.textContent = 'Cleared local transcript.'; 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();
})();