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="#f8f7f2" media="(prefers-color-scheme: light)">
  8  <meta name="theme-color" content="#151a18" 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="install-app" class="install-button" type="button" hidden>Install app</button>
 39      <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">
 40        <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>
 41      </a>
 42      <button id="open-shortcuts" class="vim-shortcuts-button" type="button" aria-label="Show Vim navigation shortcuts" title="Vim navigation shortcuts">Vim shortcuts</button>
 43      <button id="open-usage" class="usage-button" type="button" aria-haspopup="dialog" aria-label="Open API usage details">
 44        <span id="month-tokens">0</span> <span class="usage-caption">tokens</span>
 45      </button>
 46      <button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch to dark theme" title="Switch theme">
 47        <span aria-hidden="true">☾</span>
 48      </button>
 49      <button id="open-settings" class="button button-dark" type="button" aria-haspopup="dialog">
 50        <span class="status-dot" id="key-status" aria-hidden="true"></span>
 51        <span class="settings-label">Settings</span>
 52      </button>
 53    </div>
 54  </header>
 55
 56  <main class="app-shell">
 57    <aside class="history-panel" aria-label="Past conversations">
 58      <div class="history-heading">
 59        <div>
 60          <span class="eyebrow">Your library</span>
 61          <h2>Conversations</h2>
 62        </div>
 63      </div>
 64      <button id="new-map-sidebar" class="new-map-sidebar" type="button"><span aria-hidden="true">+</span> New map</button>
 65      <label class="history-search" for="conversation-search">
 66        <span aria-hidden="true">⌕</span>
 67        <input id="conversation-search" type="search" placeholder="Search conversations…" autocomplete="off" spellcheck="false">
 68        <kbd>/</kbd>
 69      </label>
 70      <div id="search-results" class="search-results" aria-live="polite" hidden></div>
 71      <nav id="history-list" class="history-list" aria-label="Saved conversations"></nav>
 72      <p id="history-empty" class="history-empty" hidden>No saved conversations yet.</p>
 73      <p class="history-storage"><span aria-hidden="true">●</span> Stored in this browser</p>
 74    </aside>
 75
 76    <section class="workspace" aria-label="Conversation">
 77      <div id="welcome" class="welcome">
 78        <div class="eyebrow">A thinking space that branches</div>
 79        <h1>StudyLab - Follow<br><em>every thought.</em></h1>
 80        <p>Ask a question, explore the answer, then branch from any earlier idea without losing your way.</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            <button id="send" class="send" type="submit" aria-label="Send message">↑</button>
122          </div>
123          <input id="file-input" type="file" accept="application/pdf,text/markdown,.pdf,.md" multiple hidden>
124        </form>
125        <p class="privacy-note">Your map stays in this browser. Added files are uploaded to the configured API.</p>
126      </div>
127    </section>
128
129    <aside class="map-panel" aria-label="Concept map">
130      <div class="map-heading">
131        <div>
132          <span class="eyebrow">Your exploration</span>
133          <h2>Concept map</h2>
134        </div>
135        <button id="collapse-map" class="icon-button" type="button" aria-label="Collapse concept map">→</button>
136      </div>
137      <p id="map-help" class="map-help">Select a node to revisit it and start a new branch.</p>
138      <div class="map-toolbar" aria-label="Concept map controls">
139        <button id="expand-all-nodes" type="button" title="Expand all branches">Expand</button>
140        <button id="collapse-all-nodes" type="button" title="Collapse all branches">Collapse</button>
141        <span class="map-toolbar-spacer"></span>
142        <button id="zoom-out-tree" type="button" aria-label="Zoom concept map out">−</button>
143        <output id="tree-zoom" aria-label="Concept map zoom">100%</output>
144        <button id="zoom-in-tree" type="button" aria-label="Zoom concept map in">+</button>
145      </div>
146      <nav id="tree" class="tree" aria-label="Question branches"></nav>
147      <div id="map-empty" class="map-empty">
148        <span class="empty-orbit">✦</span>
149        <p>Your questions will grow into a map here.</p>
150      </div>
151      <div class="map-stats">
152        <span><strong id="node-count">0</strong> nodes</span>
153        <span><strong id="branch-count">0</strong> branches</span>
154      </div>
155    </aside>
156    <button id="expand-map" class="expand-map" type="button" hidden>← Concept map</button>
157  </main>
158
159  <div id="selection-menu" class="selection-menu" role="dialog" aria-label="Ask about selected text" hidden>
160    <div class="selection-topline">
161      <span>Ask about selection</span>
162      <button id="close-selection" type="button" aria-label="Close">×</button>
163    </div>
164    <blockquote id="selection-preview"></blockquote>
165    <select id="selection-action" aria-label="Question type">
166      <option value="Explain this passage in simpler terms.">Explain simply</option>
167      <option value="What is">What is</option>
168      <option value="Go deeper into this idea and its implications.">Go deeper</option>
169      <option value="Give me a concrete example of this.">Give an example</option>
170      <option value="What assumptions does this passage make?">Challenge assumptions</option>
171      <option value="custom">Write my own…</option>
172    </select>
173    <textarea id="selection-custom" rows="2" placeholder="What would you like to ask?" hidden></textarea>
174    <button id="ask-selection" class="button button-dark" type="button">Ask</button>
175  </div>
176
177  <dialog id="settings-dialog" class="settings-dialog">
178    <form id="settings-form" method="dialog">
179      <div class="dialog-heading">
180        <div>
181          <span class="eyebrow">Connection</span>
182          <h2>API settings</h2>
183        </div>
184        <button id="close-settings" class="icon-button" type="button" aria-label="Close settings">×</button>
185      </div>
186      <p class="settings-copy">Add your provider keys once, then create model cards and choose which model Study Lab uses by default.</p>
187      <section class="api-key-settings" aria-labelledby="api-keys-title">
188        <h3 id="api-keys-title">API keys</h3>
189        <div class="api-key-grid">
190          <label>
191            OpenAI API key
192            <div class="secret-field">
193              <input id="openai-api-key" type="password" placeholder="sk-…" autocomplete="off" spellcheck="false">
194              <button class="toggle-key" type="button" data-key-input="openai-api-key" aria-label="Show OpenAI API key">Show</button>
195            </div>
196          </label>
197          <label>
198            OpenRouter API key
199            <div class="secret-field">
200              <input id="openrouter-api-key" type="password" placeholder="sk-or-v1-…" autocomplete="off" spellcheck="false">
201              <button class="toggle-key" type="button" data-key-input="openrouter-api-key" aria-label="Show OpenRouter API key">Show</button>
202            </div>
203          </label>
204        </div>
205      </section>
206      <div class="model-settings-heading">
207        <div><h3>Models</h3><p>Choose one model as the default connection.</p></div>
208        <button id="add-model" class="button button-ghost add-model" type="button" aria-label="Add model">+ Add model</button>
209      </div>
210      <div id="model-configs" class="model-configs"></div>
211      <datalist id="openai-model-options"></datalist>
212      <datalist id="openrouter-model-options"></datalist>
213      <details class="shared-settings">
214        <summary>Shared instructions</summary>
215        <label>
216          System instructions
217          <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>
218        </label>
219      </details>
220      <div class="security-callout"><strong>Local-use note</strong> — any script running on this origin can access local storage. Use a restricted API key and never deploy this frontend publicly with a saved key.</div>
221      <div class="dialog-actions">
222        <button id="clear-history" class="button button-ghost danger" type="button">Clear chat history</button>
223        <button class="button button-dark" value="save" type="submit">Save settings</button>
224      </div>
225    </form>
226  </dialog>
227
228  <dialog id="usage-dialog" class="settings-dialog usage-dialog">
229    <form method="dialog">
230      <div class="dialog-heading">
231        <div>
232          <span class="eyebrow">Local estimate</span>
233          <h2>API usage</h2>
234        </div>
235        <button id="close-usage" class="icon-button" type="button" aria-label="Close API usage">×</button>
236      </div>
237      <p class="settings-copy"><span id="usage-month"></span> · Requests made from this browser</p>
238
239      <div class="usage-summary">
240        <div class="usage-stat primary">
241          <span>Tokens</span>
242          <strong id="usage-total">0</strong>
243        </div>
244        <div class="usage-stat">
245          <span>Estimated cost</span>
246          <strong id="usage-cost">$0.00</strong>
247        </div>
248        <div class="usage-stat">
249          <span>Requests</span>
250          <strong id="usage-requests">0</strong>
251        </div>
252      </div>
253
254      <section class="usage-section" aria-labelledby="token-breakdown-title">
255        <h3 id="token-breakdown-title">Token breakdown</h3>
256        <div class="token-breakdown">
257          <span>Input <strong id="usage-input">0</strong></span>
258          <span>Cached input <strong id="usage-cached">0</strong></span>
259          <span>Output <strong id="usage-output">0</strong></span>
260        </div>
261      </section>
262
263      <section class="usage-section" aria-labelledby="models-title">
264        <h3 id="models-title">By model</h3>
265        <div id="usage-models" class="usage-models"></div>
266      </section>
267
268      <section class="usage-section" aria-labelledby="recent-title">
269        <h3 id="recent-title">Recent requests</h3>
270        <div id="usage-recent" class="usage-recent"></div>
271      </section>
272
273      <p id="usage-note" class="usage-note"></p>
274      <p class="usage-disclaimer">This is not your OpenAI invoice. It excludes requests from other browsers and applications, incomplete requests without returned usage, taxes, credits, tools, special processing, and usage recorded before this tracker was added. Prices can change.</p>
275      <div class="dialog-actions">
276        <button id="reset-usage" class="button button-ghost danger" type="button">Reset local history</button>
277        <a id="usage-dashboard" class="button button-ghost usage-dashboard" href="https://platform.openai.com/usage" target="_blank" rel="noopener noreferrer">OpenAI dashboard ↗</a>
278        <button class="button button-dark" value="close" type="submit">Done</button>
279      </div>
280    </form>
281  </dialog>
282
283  <dialog id="shortcuts-dialog" class="settings-dialog shortcuts-dialog">
284    <form method="dialog">
285      <div class="dialog-heading">
286        <div>
287          <span class="eyebrow">Navigation</span>
288          <h2>Keyboard shortcuts</h2>
289        </div>
290        <button id="close-shortcuts" class="icon-button" type="button" aria-label="Close keyboard shortcuts">×</button>
291      </div>
292      <div class="shortcut-list">
293        <span><kbd>j</kbd> / <kbd>k</kbd></span><span>Next / previous visible node</span>
294        <span><kbd>h</kbd> / <kbd>l</kbd></span><span>Parent / first child or expand</span>
295        <span><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></span><span>First / last visible node</span>
296        <span><kbd>J</kbd> / <kbd>K</kbd></span><span>Next / previous conversation</span>
297        <span><kbd>/</kbd></span><span>Search all conversations</span>
298        <span><kbd>i</kbd></span><span>Focus the question composer</span>
299        <span><kbd>Space</kbd></span><span>Collapse or expand the active branch</span>
300        <span><kbd>Esc</kbd></span><span>Leave search or input mode</span>
301        <span><kbd>?</kbd></span><span>Show this shortcut reference</span>
302      </div>
303      <div class="dialog-actions"><button class="button button-dark" value="close" type="submit">Done</button></div>
304    </form>
305  </dialog>
306
307  <div id="toast" class="toast" role="status" aria-live="polite"></div>
308
309  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
310  <script defer src="app.js"></script>
311</body>
312</html>