arnaucube
arnaucube/study-labPublic Self-hosted repository
add short-mode
arnaucube git@arnaucube.com
Mon, 31 Aug 2026 17:36:03 +0200
commit

e72b3b2ef2d9bd234e701ab5fd8267361dfd943c

parent

a9818bccc332c00535e4d273d05b4c9f70f4f497

4 files changed, 39 insertions(+), 7 deletions(-)

jump to
M app.js → app.js

@@ -9,6 +9,8 @@ const OPENROUTER_PRICES_KEY = `${APP_PREFIX}:openrouter-prices:v1`;

const OPENAI_ADMIN_SESSION_KEY = `${APP_PREFIX}:openai-admin-key`; const TREE_ZOOM_KEY = `${APP_PREFIX}:tree-zoom`; const THEME_KEY = `${APP_PREFIX}:theme`; + const SHORT_MODE_KEY = `${APP_PREFIX}:short-mode`; + const SHORT_MODE_INSTRUCTION = 'Answer concisely, focusing only on the essential points.'; const FILE_REQUEST_LIMIT = 50 * 1024 * 1024; const PRICE_CACHE_TTL = 24 * 60 * 60 * 1000; const PROVIDERS = {

@@ -46,7 +48,7 @@

const $ = (selector, root = document) => root.querySelector(selector); const els = { welcome: $('#welcome'), conversation: $('#conversation'), composer: $('#composer'), - prompt: $('#prompt'), send: $('#send'), tree: $('#tree'), mapEmpty: $('#map-empty'), + prompt: $('#prompt'), send: $('#send'), shortMode: $('#short-mode'), tree: $('#tree'), mapEmpty: $('#map-empty'), nodeCount: $('#node-count'), branchCount: $('#branch-count'), branchContext: $('#branch-context'), branchLabel: $('#branch-label'), selectionMenu: $('#selection-menu'), selectionPreview: $('#selection-preview'), selectionAction: $('#selection-action'), selectionCustom: $('#selection-custom'), settings: $('#settings-dialog'),

@@ -86,6 +88,7 @@ let searchIndex = null;

let pendingG = false; let pendingGTimer = 0; let treeZoom = loadTreeZoom(); + let shortMode = localStorage.getItem(SHORT_MODE_KEY) === 'true'; let installPrompt = null; let actualSpendRequest = 0; let modelPriceTimer = 0;

@@ -736,7 +739,11 @@ }

} } - function inputFor(map, parentId, question, provider = settings.provider) { + function questionForRequest(question, concise = false) { + return concise ? `${question}\n\n${SHORT_MODE_INSTRUCTION}` : question; + } + + function inputFor(map, parentId, question, provider = settings.provider, concise = false) { const messages = []; for (const node of pathTo(parentId, map)) { if (provider === 'openrouter') {

@@ -755,9 +762,10 @@ .filter(file => file.provider === provider)

.map(file => file.kind === 'markdown' ? { type: 'input_file', file_id: file.id } : { type: 'input_file', file_id: file.id, detail: file.detail || 'low' }); - content.push({ type: 'input_text', text: question }); + const requestQuestion = questionForRequest(question, concise); + content.push({ type: 'input_text', text: requestQuestion }); const structured = provider === 'openrouter' || content.length > 1; - messages.push(structured ? { type: 'message', role: 'user', content } : { role: 'user', content: question }); + messages.push(structured ? { type: 'message', role: 'user', content } : { role: 'user', content: requestQuestion }); return messages; }

@@ -847,6 +855,7 @@ if (!settings.apiKey) { openSettings(); showToast(`Add a ${providerLabel()} API key to begin`); return; }

const requestMap = state; const connection = { ...settings }; + const concise = shortMode; const node = { id: uid(), parentId: parentId || null, question, answer: '', status: 'loading', createdAt: Date.now(), provider: connection.provider, model: connection.model

@@ -867,7 +876,7 @@ try {

const response = await fetch(connection.apiUrl, { method: 'POST', headers: requestHeaders(true, connection), - body: JSON.stringify({ model: connection.model, instructions: connection.systemPrompt, input: inputFor(requestMap, parentId, question, connection.provider), stream: true }), + body: JSON.stringify({ model: connection.model, instructions: connection.systemPrompt, input: inputFor(requestMap, parentId, question, connection.provider, concise), stream: true }), signal: controller.signal }); if (!response.ok) {

@@ -931,6 +940,7 @@ }

function setBusy(busy) { els.prompt.disabled = busy; + els.shortMode.disabled = busy; els.send.textContent = busy ? '■' : '↑'; els.send.setAttribute('aria-label', busy ? 'Stop response' : 'Send message'); els.uploadFile.disabled = busy || uploadingFiles;

@@ -1571,6 +1581,11 @@ });

els.prompt.addEventListener('input', () => { els.prompt.style.height = 'auto'; els.prompt.style.height = `${Math.min(els.prompt.scrollHeight, 180)}px`; + }); + els.shortMode.checked = shortMode; + els.shortMode.addEventListener('change', () => { + shortMode = els.shortMode.checked; + localStorage.setItem(SHORT_MODE_KEY, String(shortMode)); }); document.addEventListener('click', event => { const starter = event.target.closest('.starter');
M index.html → index.html

@@ -118,7 +118,14 @@ <small id="active-provider">OpenAI</small>

</button> <span class="hint"><kbd>Enter</kbd> send · <kbd>Shift</kbd> + <kbd>Enter</kbd> new line</span> </div> - <button id="send" class="send" type="submit" aria-label="Send message">↑</button> + <div class="composer-actions"> + <label class="short-mode" title="Ask for concise answers"> + <span>Short answer</span> + <input id="short-mode" type="checkbox" role="switch" aria-label="Short answer mode"> + <span class="short-mode-track" aria-hidden="true"><span></span></span> + </label> + <button id="send" class="send" type="submit" aria-label="Send message">↑</button> + </div> </div> <input id="file-input" type="file" accept="application/pdf,text/markdown,.pdf,.md" multiple hidden> </form>
M service-worker.js → service-worker.js

@@ -1,4 +1,4 @@

-const CACHE_NAME = 'study-lab-shell-v4'; +const CACHE_NAME = 'study-lab-shell-v5'; const APP_SHELL = [ './', './index.html',
M styles.css → styles.css

@@ -202,6 +202,15 @@ .upload-spinner { width: 10px; height: 10px; border: 2px solid var(--line); border-top-color: var(--sage-dark); border-radius: 50%; animation: spin .7s linear infinite; }

@keyframes spin { to { transform: rotate(360deg); } } .hint { font-size: .68rem; color: #8b918e; } kbd { font-family: inherit; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px; padding: 1px 4px; background: #fafafa; } +.composer-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; } +.short-mode { display: flex; align-items: center; gap: 6px; color: var(--muted); cursor: pointer; font-size: .68rem; font-weight: 700; user-select: none; } +.short-mode input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; } +.short-mode-track { position: relative; width: 30px; height: 18px; border: 1px solid var(--line); border-radius: 999px; background: var(--paper); transition: background .15s, border-color .15s; } +.short-mode-track span { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--muted); transition: transform .15s, background .15s; } +.short-mode input:checked + .short-mode-track { border-color: var(--sage-dark); background: var(--sage-dark); } +.short-mode input:checked + .short-mode-track span { background: white; transform: translateX(12px); } +.short-mode input:focus-visible + .short-mode-track { outline: 2px solid var(--sage-dark); outline-offset: 2px; } +.short-mode:has(input:disabled) { opacity: .45; cursor: default; } .send { border: 0; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; background: var(--ink); color: white; cursor: pointer; font-size: 1.15rem; } .send:disabled { opacity: .35; cursor: default; } .privacy-note { max-width: 850px; margin: 7px auto 0; text-align: center; color: #8b918e; font-size: .66rem; }

@@ -425,6 +434,7 @@ :root[data-theme="dark"] .answer-body pre { background: #0d110f; color: #e9eeeb; }

:root[data-theme="dark"] .mini-action:hover { background: #252d29; } :root[data-theme="dark"] .composer { background: var(--card); border-color: #3a4640; box-shadow: 0 9px 32px rgba(0,0,0,.22); } :root[data-theme="dark"] .composer-wrap { background: linear-gradient(transparent, var(--paper) 22%, var(--paper)); } +:root[data-theme="dark"] .short-mode-track { background: #252c29; } :root[data-theme="dark"] kbd { background: #252c29; } :root[data-theme="dark"] .branch-context { background: #223029; border-color: #35493f; } :root[data-theme="dark"] .icon-button { background: rgba(255,255,255,.035); }