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 <title>Study Lab — Branch your thinking</title>
8 <script>
9 try {
10 const themeKey = 'study-lab:theme';
11 const legacyThemeKey = `${['study', 'pal'].join('-')}:theme`;
12 const savedTheme = localStorage.getItem(themeKey) || localStorage.getItem(legacyThemeKey);
13 if (!localStorage.getItem(themeKey) && savedTheme) localStorage.setItem(themeKey, savedTheme);
14 document.documentElement.dataset.theme = savedTheme ||
15 (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
16 } catch { document.documentElement.dataset.theme = 'light'; }
17 </script>
18 <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
19 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
20 <link rel="stylesheet" href="styles.css">
21</head>
22<body>
23 <header class="topbar">
24 <div class="brand-group">
25 <button id="toggle-history" class="history-toggle" type="button" aria-label="Toggle conversations sidebar" aria-expanded="true">☰</button>
26 <a id="study-lab-home" class="brand" href="#" aria-label="Open a new map">
27 <span class="brand-mark" aria-hidden="true">S</span>
28 <span>Study Lab</span>
29 </a>
30 </div>
31 <div class="top-actions">
32 <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">
33 <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>
34 </a>
35 <button id="open-shortcuts" class="vim-shortcuts-button" type="button" aria-label="Show Vim navigation shortcuts" title="Vim navigation shortcuts">Vim shortcuts</button>
36 <button id="open-usage" class="usage-button" type="button" aria-haspopup="dialog" aria-label="Open API usage details">
37 <span id="month-tokens">0</span> <span class="usage-caption">tokens</span>
38 </button>
39 <button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch to dark theme" title="Switch theme">
40 <span aria-hidden="true">☾</span>
41 </button>
42 <button id="open-settings" class="button button-dark" type="button" aria-haspopup="dialog">
43 <span class="status-dot" id="key-status" aria-hidden="true"></span>
44 <span class="settings-label">Settings</span>
45 </button>
46 </div>
47 </header>
48
49 <main class="app-shell">
50 <aside class="history-panel" aria-label="Past conversations">
51 <div class="history-heading">
52 <div>
53 <span class="eyebrow">Your library</span>
54 <h2>Conversations</h2>
55 </div>
56 </div>
57 <button id="new-map-sidebar" class="new-map-sidebar" type="button"><span aria-hidden="true">+</span> New map</button>
58 <label class="history-search" for="conversation-search">
59 <span aria-hidden="true">⌕</span>
60 <input id="conversation-search" type="search" placeholder="Search conversations…" autocomplete="off" spellcheck="false">
61 <kbd>/</kbd>
62 </label>
63 <div id="search-results" class="search-results" aria-live="polite" hidden></div>
64 <nav id="history-list" class="history-list" aria-label="Saved conversations"></nav>
65 <p id="history-empty" class="history-empty" hidden>No saved conversations yet.</p>
66 <p class="history-storage"><span aria-hidden="true">●</span> Stored in this browser</p>
67 </aside>
68
69 <section class="workspace" aria-label="Conversation">
70 <div id="welcome" class="welcome">
71 <div class="eyebrow">A thinking space that branches</div>
72 <h1>StudyLab - Follow<br><em>every thought.</em></h1>
73 <p>Ask a question, explore the answer, then branch from any earlier idea without losing your way.</p>
74 <div class="starter-grid" aria-label="Example questions">
75 <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.">
76 <span class="starter-icon">⊆</span>
77 <strong>Explain the Tower Law from Galois Theory</strong>
78 <small>Degrees of extensions in Galois theory</small>
79 </button>
80 <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.">
81 <span class="starter-icon">V</span>
82 <strong>Connect ideals and geometry</strong>
83 <small>Understand the Nullstellensatz correspondence</small>
84 </button>
85 <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.">
86 <span class="starter-icon">f</span>
87 <strong>Study fibers of algebraic varieties</strong>
88 <small>Generic and special fibers of varieties</small>
89 </button>
90 </div>
91 </div>
92
93 <div id="conversation" class="conversation" aria-live="polite"></div>
94
95 <div class="composer-wrap">
96 <div id="branch-context" class="branch-context" hidden>
97 <span>Branching from <strong id="branch-label"></strong></span>
98 <button id="clear-branch" type="button" aria-label="Return to latest node">×</button>
99 </div>
100 <form id="composer" class="composer">
101 <div id="attachments" class="attachments" aria-label="PDFs in this map" hidden></div>
102 <textarea id="prompt" rows="1" placeholder="Ask anything…" aria-label="Message" maxlength="30000"></textarea>
103 <div class="composer-footer">
104 <div class="composer-tools">
105 <button id="upload-pdf" class="upload-pdf" type="button" title="Add PDFs to this map" aria-label="Add PDFs to this map">
106 <span aria-hidden="true">+</span> PDF
107 </button>
108 <button id="active-connection" class="active-connection" type="button" aria-label="Open API settings">
109 <span id="active-model">gpt-5-mini</span>
110 <small id="active-provider">OpenAI</small>
111 </button>
112 <span class="hint"><kbd>Enter</kbd> send · <kbd>Shift</kbd> + <kbd>Enter</kbd> new line</span>
113 </div>
114 <button id="send" class="send" type="submit" aria-label="Send message">↑</button>
115 </div>
116 <input id="pdf-input" type="file" accept="application/pdf,.pdf" multiple hidden>
117 </form>
118 <p class="privacy-note">Your map stays in this browser. Added PDFs are uploaded to the configured API.</p>
119 </div>
120 </section>
121
122 <aside class="map-panel" aria-label="Concept map">
123 <div class="map-heading">
124 <div>
125 <span class="eyebrow">Your exploration</span>
126 <h2>Concept map</h2>
127 </div>
128 <button id="collapse-map" class="icon-button" type="button" aria-label="Collapse concept map">→</button>
129 </div>
130 <p id="map-help" class="map-help">Select a node to revisit it and start a new branch.</p>
131 <div class="map-toolbar" aria-label="Concept map controls">
132 <button id="expand-all-nodes" type="button" title="Expand all branches">Expand</button>
133 <button id="collapse-all-nodes" type="button" title="Collapse all branches">Collapse</button>
134 <span class="map-toolbar-spacer"></span>
135 <button id="zoom-out-tree" type="button" aria-label="Zoom concept map out">−</button>
136 <output id="tree-zoom" aria-label="Concept map zoom">100%</output>
137 <button id="zoom-in-tree" type="button" aria-label="Zoom concept map in">+</button>
138 </div>
139 <nav id="tree" class="tree" aria-label="Question branches"></nav>
140 <div id="map-empty" class="map-empty">
141 <span class="empty-orbit">✦</span>
142 <p>Your questions will grow into a map here.</p>
143 </div>
144 <div class="map-stats">
145 <span><strong id="node-count">0</strong> nodes</span>
146 <span><strong id="branch-count">0</strong> branches</span>
147 </div>
148 </aside>
149 <button id="expand-map" class="expand-map" type="button" hidden>← Concept map</button>
150 </main>
151
152 <div id="selection-menu" class="selection-menu" role="dialog" aria-label="Ask about selected text" hidden>
153 <div class="selection-topline">
154 <span>Ask about selection</span>
155 <button id="close-selection" type="button" aria-label="Close">×</button>
156 </div>
157 <blockquote id="selection-preview"></blockquote>
158 <select id="selection-action" aria-label="Question type">
159 <option value="Explain this passage in simpler terms.">Explain simply</option>
160 <option value="What is">What is</option>
161 <option value="Go deeper into this idea and its implications.">Go deeper</option>
162 <option value="Give me a concrete example of this.">Give an example</option>
163 <option value="What assumptions does this passage make?">Challenge assumptions</option>
164 <option value="custom">Write my own…</option>
165 </select>
166 <textarea id="selection-custom" rows="2" placeholder="What would you like to ask?" hidden></textarea>
167 <button id="ask-selection" class="button button-dark" type="button">Ask</button>
168 </div>
169
170 <dialog id="settings-dialog" class="settings-dialog">
171 <form id="settings-form" method="dialog">
172 <div class="dialog-heading">
173 <div>
174 <span class="eyebrow">Connection</span>
175 <h2>API settings</h2>
176 </div>
177 <button id="close-settings" class="icon-button" type="button" aria-label="Close settings">×</button>
178 </div>
179 <p class="settings-copy">Choose the active provider, then configure both connections below. Each provider keeps its own key, model, and endpoint.</p>
180 <label class="active-provider-field">
181 Active provider
182 <select id="provider">
183 <option value="openai">OpenAI</option>
184 <option value="openrouter">OpenRouter</option>
185 </select>
186 </label>
187 <div class="provider-configs">
188 <section class="provider-config" data-provider-config="openai">
189 <div class="provider-config-heading">
190 <div><span class="provider-logo">OAI</span><h3>OpenAI</h3></div>
191 <span class="provider-active">Selected</span>
192 </div>
193 <label>
194 API key
195 <div class="secret-field">
196 <input id="openai-api-key" type="password" placeholder="sk-…" autocomplete="off" spellcheck="false">
197 <button class="toggle-key" type="button" data-key-input="openai-api-key" aria-label="Show OpenAI API key">Show</button>
198 </div>
199 </label>
200 <label>
201 Model
202 <input id="openai-model" type="text" value="gpt-5-mini" placeholder="gpt-5-mini" list="openai-model-options" spellcheck="false">
203 <datalist id="openai-model-options"></datalist>
204 </label>
205 <details class="provider-advanced">
206 <summary>Endpoint</summary>
207 <label>
208 Responses API endpoint
209 <input id="openai-api-url" type="url" value="https://api.openai.com/v1/responses" spellcheck="false">
210 </label>
211 </details>
212 </section>
213 <section class="provider-config" data-provider-config="openrouter">
214 <div class="provider-config-heading">
215 <div><span class="provider-logo">OR</span><h3>OpenRouter</h3></div>
216 <span class="provider-active">Selected</span>
217 </div>
218 <label>
219 API key
220 <div class="secret-field">
221 <input id="openrouter-api-key" type="password" placeholder="sk-or-v1-…" autocomplete="off" spellcheck="false">
222 <button class="toggle-key" type="button" data-key-input="openrouter-api-key" aria-label="Show OpenRouter API key">Show</button>
223 </div>
224 </label>
225 <label>
226 Model
227 <input id="openrouter-model" type="text" value="openai/gpt-5-mini" placeholder="provider/model" list="openrouter-model-options" spellcheck="false">
228 <datalist id="openrouter-model-options"></datalist>
229 <small class="field-help">OpenRouter model IDs use the provider/model format.</small>
230 </label>
231 <details class="provider-advanced">
232 <summary>Endpoint</summary>
233 <label>
234 Responses API endpoint
235 <input id="openrouter-api-url" type="url" value="https://openrouter.ai/api/v1/responses" spellcheck="false">
236 </label>
237 </details>
238 </section>
239 </div>
240 <details class="shared-settings">
241 <summary>Shared instructions</summary>
242 <label>
243 System instructions
244 <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>
245 </label>
246 </details>
247 <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>
248 <div class="dialog-actions">
249 <button id="clear-history" class="button button-ghost danger" type="button">Clear chat history</button>
250 <button id="forget-key" class="button button-ghost danger" type="button">Forget selected key</button>
251 <button class="button button-dark" value="save" type="submit">Save settings</button>
252 </div>
253 </form>
254 </dialog>
255
256 <dialog id="usage-dialog" class="settings-dialog usage-dialog">
257 <form method="dialog">
258 <div class="dialog-heading">
259 <div>
260 <span class="eyebrow">Local estimate</span>
261 <h2>API usage</h2>
262 </div>
263 <button id="close-usage" class="icon-button" type="button" aria-label="Close API usage">×</button>
264 </div>
265 <p class="settings-copy"><span id="usage-month"></span> · Requests made from this browser</p>
266
267 <div class="usage-summary">
268 <div class="usage-stat primary">
269 <span>Tokens</span>
270 <strong id="usage-total">0</strong>
271 </div>
272 <div class="usage-stat">
273 <span>Estimated cost</span>
274 <strong id="usage-cost">$0.00</strong>
275 </div>
276 <div class="usage-stat">
277 <span>Requests</span>
278 <strong id="usage-requests">0</strong>
279 </div>
280 </div>
281
282 <section class="usage-section" aria-labelledby="token-breakdown-title">
283 <h3 id="token-breakdown-title">Token breakdown</h3>
284 <div class="token-breakdown">
285 <span>Input <strong id="usage-input">0</strong></span>
286 <span>Cached input <strong id="usage-cached">0</strong></span>
287 <span>Output <strong id="usage-output">0</strong></span>
288 </div>
289 </section>
290
291 <section class="usage-section" aria-labelledby="models-title">
292 <h3 id="models-title">By model</h3>
293 <div id="usage-models" class="usage-models"></div>
294 </section>
295
296 <section class="usage-section" aria-labelledby="recent-title">
297 <h3 id="recent-title">Recent requests</h3>
298 <div id="usage-recent" class="usage-recent"></div>
299 </section>
300
301 <p id="usage-note" class="usage-note"></p>
302 <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>
303 <div class="dialog-actions">
304 <button id="reset-usage" class="button button-ghost danger" type="button">Reset local history</button>
305 <a id="usage-dashboard" class="button button-ghost usage-dashboard" href="https://platform.openai.com/usage" target="_blank" rel="noopener noreferrer">OpenAI dashboard ↗</a>
306 <button class="button button-dark" value="close" type="submit">Done</button>
307 </div>
308 </form>
309 </dialog>
310
311 <dialog id="shortcuts-dialog" class="settings-dialog shortcuts-dialog">
312 <form method="dialog">
313 <div class="dialog-heading">
314 <div>
315 <span class="eyebrow">Navigation</span>
316 <h2>Keyboard shortcuts</h2>
317 </div>
318 <button id="close-shortcuts" class="icon-button" type="button" aria-label="Close keyboard shortcuts">×</button>
319 </div>
320 <div class="shortcut-list">
321 <span><kbd>j</kbd> / <kbd>k</kbd></span><span>Next / previous visible node</span>
322 <span><kbd>h</kbd> / <kbd>l</kbd></span><span>Parent / first child or expand</span>
323 <span><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></span><span>First / last visible node</span>
324 <span><kbd>J</kbd> / <kbd>K</kbd></span><span>Next / previous conversation</span>
325 <span><kbd>/</kbd></span><span>Search all conversations</span>
326 <span><kbd>i</kbd></span><span>Focus the question composer</span>
327 <span><kbd>Space</kbd></span><span>Collapse or expand the active branch</span>
328 <span><kbd>Esc</kbd></span><span>Leave search or input mode</span>
329 <span><kbd>?</kbd></span><span>Show this shortcut reference</span>
330 </div>
331 <div class="dialog-actions"><button class="button button-dark" value="close" type="submit">Done</button></div>
332 </form>
333 </dialog>
334
335 <div id="toast" class="toast" role="status" aria-live="polite"></div>
336
337 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
338 <script defer src="app.js"></script>
339</body>
340</html>