arnaucube
arnaucube/study-labPublic Self-hosted repository
study-lab/index.html
index.htmlRaw
  1<!doctype html>
  2<html lang="en">
  3<head>
  4  <meta charset="utf-8">
  5  <meta name="viewport" content="width=device-width, initial-scale=1">
  6  <meta name="color-scheme" content="light dark">
  7  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
  8  <meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)">
  9  <meta name="description" content="A local-first, branching AI study space for following every thought.">
 10  <title>Study Lab — Branch your thinking</title>
 11  <script>
 12    try {
 13      const themeKey = 'study-lab:theme';
 14      const legacyThemeKey = `${['study', 'pal'].join('-')}:theme`;
 15      const savedTheme = localStorage.getItem(themeKey) || localStorage.getItem(legacyThemeKey);
 16      if (!localStorage.getItem(themeKey) && savedTheme) localStorage.setItem(themeKey, savedTheme);
 17      document.documentElement.dataset.theme = savedTheme ||
 18        (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
 19    } catch { document.documentElement.dataset.theme = 'light'; }
 20  </script>
 21  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
 22  <link rel="manifest" href="manifest.webmanifest">
 23  <link rel="icon" href="icons/icon-192.png" sizes="192x192" type="image/png">
 24  <link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
 25  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
 26  <link rel="stylesheet" href="styles.css">
 27</head>
 28<body>
 29  <header class="topbar">
 30    <div class="brand-group">
 31      <button id="toggle-history" class="history-toggle" type="button" aria-label="Toggle conversations sidebar" aria-expanded="true">☰</button>
 32      <a id="study-lab-home" class="brand" href="#" aria-label="Open a new map">
 33        <span class="brand-mark" aria-hidden="true">S</span>
 34        <span>Study Lab</span>
 35      </a>
 36    </div>
 37    <div class="top-actions">
 38      <button id="export-pdf" class="export-button" type="button" title="Export the selected branch as PDF" hidden>Export PDF</button>
 39      <button id="install-app" class="install-button" type="button" hidden>Install app</button>
 40      <a class="github-button" href="https://github.com/arnaucube/study-lab" target="_blank" rel="noopener noreferrer" aria-label="Open Study Lab on GitHub" title="View source on GitHub">
 41        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .7a11.5 11.5 0 0 0-3.64 22.41c.58.1.79-.25.79-.56v-2.23c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.78 1.2 1.78 1.2 1.04 1.77 2.72 1.26 3.38.96.1-.75.4-1.26.74-1.55-2.57-.29-5.27-1.28-5.27-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.16 1.18a10.96 10.96 0 0 1 5.76 0c2.19-1.49 3.16-1.18 3.16-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.41-2.71 5.38-5.29 5.67.42.36.79 1.06.79 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .7Z"/></svg>
 42      </a>
 43      <button id="open-shortcuts" class="vim-shortcuts-button" type="button" aria-label="Show Vim navigation shortcuts" title="Vim navigation shortcuts">Vim shortcuts</button>
 44      <button id="open-usage" class="usage-button" type="button" aria-haspopup="dialog" aria-label="Open API usage details">
 45        <span id="month-tokens">0</span> <span class="usage-caption">tokens</span>
 46      </button>
 47      <button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch to dark theme" title="Switch theme">
 48        <span aria-hidden="true">☾</span>
 49      </button>
 50      <button id="open-settings" class="button button-dark" type="button" aria-haspopup="dialog">
 51        <span class="status-dot" id="key-status" aria-hidden="true"></span>
 52        <span class="settings-label">Settings</span>
 53      </button>
 54    </div>
 55  </header>
 56
 57  <main class="app-shell">
 58    <aside class="history-panel" aria-label="Past conversations">
 59      <div class="history-heading">
 60        <div>
 61          <span class="eyebrow">Your library</span>
 62          <h2>Conversations</h2>
 63        </div>
 64      </div>
 65      <button id="new-map-sidebar" class="new-map-sidebar" type="button"><span aria-hidden="true">+</span> New map</button>
 66      <label class="history-search" for="conversation-search">
 67        <span aria-hidden="true">⌕</span>
 68        <input id="conversation-search" type="search" placeholder="Search conversations…" autocomplete="off" spellcheck="false">
 69        <kbd>/</kbd>
 70      </label>
 71      <div id="search-results" class="search-results" aria-live="polite" hidden></div>
 72      <nav id="history-list" class="history-list" aria-label="Saved conversations"></nav>
 73      <p id="history-empty" class="history-empty" hidden>No saved conversations yet.</p>
 74      <p class="history-storage"><span aria-hidden="true">●</span> Stored in this browser</p>
 75    </aside>
 76
 77    <section class="workspace" aria-label="Conversation">
 78      <div id="welcome" class="welcome">
 79        <h1>StudyLab - Follow every thought.</h1>
 80        <p>Ask a question. Continue from any earlier answer to start a new branch.</p>
 81        <div class="starter-grid" aria-label="Example questions">
 82          <button class="starter" data-prompt="Explain and prove the Tower Law for finite field extensions K ⊆ F ⊆ L. Show how bases compose, then work through the example Q ⊆ Q(√2) ⊆ Q(√2, √3) and relate it to Galois theory.">
 83            <span class="starter-icon">⊆</span>
 84            <strong>Explain the Tower Law from Galois Theory</strong>
 85            <small>Degrees of extensions in Galois theory</small>
 86          </button>
 87          <button class="starter" data-prompt="Over an algebraically closed field k, explain and prove the affine algebraic set–radical ideal correspondence from Hilbert's Nullstellensatz, carefully showing how the algebra and geometry reverse inclusions.">
 88            <span class="starter-icon">V</span>
 89            <strong>Connect ideals and geometry</strong>
 90            <small>Understand the Nullstellensatz correspondence</small>
 91          </button>
 92          <button class="starter" data-prompt="Let f: X → Y be a dominant morphism of irreducible algebraic varieties. Prove the fiber dimension theorem, explain why the generic fiber has dimension dim(X) − dim(Y), and give an example where special fibers have larger dimension.">
 93            <span class="starter-icon">f</span>
 94            <strong>Study fibers of algebraic varieties</strong>
 95            <small>Generic and special fibers of varieties</small>
 96          </button>
 97        </div>
 98      </div>
 99
100      <div id="conversation" class="conversation" aria-live="polite"></div>
101
102      <div class="composer-wrap">
103        <div id="branch-context" class="branch-context" hidden>
104          <span>Branching from <strong id="branch-label"></strong></span>
105          <button id="clear-branch" type="button" aria-label="Return to latest node">×</button>
106        </div>
107        <form id="composer" class="composer">
108          <div id="attachments" class="attachments" aria-label="Files in this map" hidden></div>
109          <textarea id="prompt" rows="1" placeholder="Ask anything…" aria-label="Message" maxlength="30000"></textarea>
110          <div class="composer-footer">
111            <div class="composer-tools">
112              <button id="upload-file" class="upload-file" type="button" title="Add PDF or Markdown files to this map" aria-label="Add PDF or Markdown files to this map">
113                <span aria-hidden="true">+</span> PDF / MD
114              </button>
115              <button id="active-connection" class="active-connection" type="button" aria-label="Open API settings">
116                <span id="active-model">gpt-5-mini</span>
117                <small id="active-provider">OpenAI</small>
118              </button>
119              <span class="hint"><kbd>Enter</kbd> send · <kbd>Shift</kbd> + <kbd>Enter</kbd> new line</span>
120            </div>
121            <div class="composer-actions">
122              <label class="short-mode" title="Ask for concise answers">
123                <span>Short answer</span>
124                <input id="short-mode" type="checkbox" role="switch" aria-label="Short answer mode">
125                <span class="short-mode-track" aria-hidden="true"><span></span></span>
126              </label>
127              <button id="send" class="send" type="submit" aria-label="Send message">↑</button>
128            </div>
129          </div>
130          <input id="file-input" type="file" accept="application/pdf,text/markdown,.pdf,.md" multiple hidden>
131        </form>
132        <p class="privacy-note">Your map stays in this browser. Added files are uploaded to the configured API.</p>
133      </div>
134    </section>
135
136    <aside class="map-panel" aria-label="Concept map">
137      <div class="map-heading">
138        <div>
139          <span class="eyebrow">Your exploration</span>
140          <h2>Concept map</h2>
141        </div>
142        <button id="collapse-map" class="icon-button" type="button" aria-label="Collapse concept map">→</button>
143      </div>
144      <p id="map-help" class="map-help">Select a node to revisit it and start a new branch.</p>
145      <div class="map-toolbar" aria-label="Concept map controls">
146        <button id="expand-all-nodes" type="button" title="Expand all branches">Expand</button>
147        <button id="collapse-all-nodes" type="button" title="Collapse all branches">Collapse</button>
148        <span class="map-toolbar-spacer"></span>
149        <button id="zoom-out-tree" type="button" aria-label="Zoom concept map out">−</button>
150        <output id="tree-zoom" aria-label="Concept map zoom">100%</output>
151        <button id="zoom-in-tree" type="button" aria-label="Zoom concept map in">+</button>
152      </div>
153      <nav id="tree" class="tree" aria-label="Question branches"></nav>
154      <div id="map-empty" class="map-empty">
155        <span class="empty-orbit">✦</span>
156        <p>Your questions will grow into a map here.</p>
157      </div>
158      <div class="map-stats">
159        <span><strong id="node-count">0</strong> nodes</span>
160        <span><strong id="branch-count">0</strong> branches</span>
161      </div>
162    </aside>
163    <button id="expand-map" class="expand-map" type="button" hidden>← Concept map</button>
164  </main>
165
166  <div id="selection-menu" class="selection-menu" role="dialog" aria-label="Ask about selected text" hidden>
167    <div class="selection-topline">
168      <span>Ask about selection</span>
169      <button id="close-selection" type="button" aria-label="Close">×</button>
170    </div>
171    <blockquote id="selection-preview"></blockquote>
172    <select id="selection-action" aria-label="Question type">
173      <option value="Explain this passage in simpler terms.">Explain simply</option>
174      <option value="What is">What is</option>
175      <option value="Go deeper into this idea and its implications.">Go deeper</option>
176      <option value="Give me a concrete example of this.">Give an example</option>
177      <option value="What assumptions does this passage make?">Challenge assumptions</option>
178      <option value="custom">Write my own…</option>
179    </select>
180    <textarea id="selection-custom" rows="2" placeholder="What would you like to ask?" hidden></textarea>
181    <button id="ask-selection" class="button button-dark" type="button">Ask</button>
182  </div>
183
184  <dialog id="settings-dialog" class="settings-dialog">
185    <form id="settings-form" method="dialog">
186      <div class="dialog-heading">
187        <div>
188          <span class="eyebrow">Connection</span>
189          <h2>API settings</h2>
190        </div>
191        <button id="close-settings" class="icon-button" type="button" aria-label="Close settings">×</button>
192      </div>
193      <p class="settings-copy">Add your provider keys once, then create model cards and choose which model Study Lab uses by default.</p>
194      <section class="api-key-settings" aria-labelledby="api-keys-title">
195        <h3 id="api-keys-title">API keys</h3>
196        <div class="api-key-grid">
197          <label>
198            OpenAI API key
199            <div class="secret-field">
200              <input id="openai-api-key" type="password" placeholder="sk-…" autocomplete="off" spellcheck="false">
201              <button class="toggle-key" type="button" data-key-input="openai-api-key" aria-label="Show OpenAI API key">Show</button>
202            </div>
203          </label>
204          <label>
205            OpenRouter API key
206            <div class="secret-field">
207              <input id="openrouter-api-key" type="password" placeholder="sk-or-v1-…" autocomplete="off" spellcheck="false">
208              <button class="toggle-key" type="button" data-key-input="openrouter-api-key" aria-label="Show OpenRouter API key">Show</button>
209            </div>
210          </label>
211          <label class="admin-key-setting">
212            OpenAI Admin API key <span class="optional-label">billing only</span>
213            <div class="secret-field">
214              <input id="openai-admin-key" type="password" placeholder="sk-admin-…" autocomplete="off" spellcheck="false">
215              <button class="toggle-key" type="button" data-key-input="openai-admin-key" aria-label="Show OpenAI Admin API key">Show</button>
216            </div>
217            <small class="field-help">Required for actual organization spend. Kept only for this browser tab and never saved to local storage.</small>
218          </label>
219        </div>
220      </section>
221      <div class="model-settings-heading">
222        <div><h3>Models</h3><p>Choose one model as the default connection.</p></div>
223        <button id="add-model" class="button button-ghost add-model" type="button" aria-label="Add model">+ Add model</button>
224      </div>
225      <div id="model-configs" class="model-configs"></div>
226      <datalist id="openai-model-options"></datalist>
227      <datalist id="openrouter-model-options"></datalist>
228      <details class="shared-settings">
229        <summary>Shared instructions</summary>
230        <label>
231          System instructions
232          <textarea id="system-prompt" rows="3">You are a patient technical tutor. Use clear Markdown and LaTeX where useful. Use mathematical language, assume graduate level knowledge. Build on the conversation context and explain assumptions.</textarea>
233        </label>
234      </details>
235      <div class="security-callout"><strong>Local-use note</strong> — any script running on this origin can access browser storage. Use restricted inference keys, treat an OpenAI Admin key as highly privileged, and never deploy this frontend publicly with saved keys.</div>
236      <div class="dialog-actions">
237        <button id="clear-history" class="button button-ghost danger" type="button">Clear chat history</button>
238        <button class="button button-dark" value="save" type="submit">Save settings</button>
239      </div>
240    </form>
241  </dialog>
242
243  <dialog id="usage-dialog" class="settings-dialog usage-dialog">
244    <form method="dialog">
245      <div class="dialog-heading">
246        <div>
247          <span class="eyebrow">Provider billing</span>
248          <h2>API usage</h2>
249        </div>
250        <button id="close-usage" class="icon-button" type="button" aria-label="Close API usage">×</button>
251      </div>
252      <p class="settings-copy"><span id="usage-month"></span> · Month to date</p>
253
254      <section class="actual-spend" aria-labelledby="actual-spend-title">
255        <div class="usage-section-heading">
256          <div>
257            <h3 id="actual-spend-title">Actual provider spend</h3>
258            <p>Retrieved directly from each provider.</p>
259          </div>
260          <button id="refresh-actual-spend" class="button button-ghost" type="button">Refresh</button>
261        </div>
262        <div class="actual-spend-grid" aria-live="polite">
263          <div class="actual-spend-card">
264            <span>OpenAI organization</span>
265            <strong id="actual-openai-cost">—</strong>
266            <small id="actual-openai-status">Admin key required</small>
267          </div>
268          <div class="actual-spend-card">
269            <span>OpenRouter API key</span>
270            <strong id="actual-openrouter-cost">—</strong>
271            <small id="actual-openrouter-status">API key required</small>
272          </div>
273          <div class="actual-spend-card total">
274            <span>Combined available</span>
275            <strong id="actual-combined-cost">—</strong>
276            <small id="actual-combined-status">Waiting to refresh</small>
277          </div>
278        </div>
279        <p class="actual-spend-note">OpenAI reports organization-wide costs and requires an organization-owner Admin API key. OpenRouter reports usage for the configured API key.</p>
280      </section>
281
282      <div class="usage-section-heading local-usage-heading">
283        <div><h3>Local browser estimate</h3><p>Only completed requests recorded by Study Lab in this browser.</p></div>
284      </div>
285
286      <div class="usage-summary">
287        <div class="usage-stat primary">
288          <span>Tokens</span>
289          <strong id="usage-total">0</strong>
290        </div>
291        <div class="usage-stat">
292          <span>Estimated cost</span>
293          <strong id="usage-cost">$0.00</strong>
294        </div>
295        <div class="usage-stat">
296          <span>Requests</span>
297          <strong id="usage-requests">0</strong>
298        </div>
299      </div>
300
301      <section class="usage-section" aria-labelledby="token-breakdown-title">
302        <h3 id="token-breakdown-title">Token breakdown</h3>
303        <div class="token-breakdown">
304          <span>Input <strong id="usage-input">0</strong></span>
305          <span>Cached input <strong id="usage-cached">0</strong></span>
306          <span>Output <strong id="usage-output">0</strong></span>
307        </div>
308      </section>
309
310      <section class="usage-section" aria-labelledby="models-title">
311        <h3 id="models-title">By model</h3>
312        <div id="usage-models" class="usage-models"></div>
313      </section>
314
315      <section class="usage-section" aria-labelledby="recent-title">
316        <h3 id="recent-title">Recent requests</h3>
317        <div id="usage-recent" class="usage-recent"></div>
318      </section>
319
320      <p id="usage-note" class="usage-note"></p>
321      <p class="usage-disclaimer">Provider totals can lag behind recent requests and may exclude taxes, credit-purchase fees, or other invoice adjustments. The local estimate excludes requests from other browsers and applications, incomplete requests without returned usage, and usage recorded before this tracker was added.</p>
322      <div class="dialog-actions">
323        <button id="reset-usage" class="button button-ghost danger" type="button">Reset local history</button>
324        <a class="button button-ghost usage-dashboard" href="https://platform.openai.com/usage" target="_blank" rel="noopener noreferrer">OpenAI ↗</a>
325        <a class="button button-ghost usage-dashboard" href="https://openrouter.ai/activity" target="_blank" rel="noopener noreferrer">OpenRouter ↗</a>
326        <button class="button button-dark" value="close" type="submit">Done</button>
327      </div>
328    </form>
329  </dialog>
330
331  <dialog id="shortcuts-dialog" class="settings-dialog shortcuts-dialog">
332    <form method="dialog">
333      <div class="dialog-heading">
334        <div>
335          <span class="eyebrow">Navigation</span>
336          <h2>Keyboard shortcuts</h2>
337        </div>
338        <button id="close-shortcuts" class="icon-button" type="button" aria-label="Close keyboard shortcuts">×</button>
339      </div>
340      <div class="shortcut-list">
341        <span><kbd>j</kbd> / <kbd>k</kbd></span><span>Next / previous visible node</span>
342        <span><kbd>h</kbd> / <kbd>l</kbd></span><span>Parent / first child or expand</span>
343        <span><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></span><span>First / last visible node</span>
344        <span><kbd>J</kbd> / <kbd>K</kbd></span><span>Next / previous conversation</span>
345        <span><kbd>/</kbd></span><span>Search all conversations</span>
346        <span><kbd>i</kbd></span><span>Focus the question composer</span>
347        <span><kbd>Space</kbd></span><span>Collapse or expand the active branch</span>
348        <span><kbd>Esc</kbd></span><span>Leave search or input mode</span>
349        <span><kbd>?</kbd></span><span>Show this shortcut reference</span>
350      </div>
351      <div class="dialog-actions"><button class="button button-dark" value="close" type="submit">Done</button></div>
352    </form>
353  </dialog>
354
355  <div id="toast" class="toast" role="status" aria-live="polite"></div>
356
357  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
358  <script defer src="app.js"></script>
359</body>
360</html>