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 <span class="hint"><kbd>Enter</kbd> send · <kbd>Shift</kbd> + <kbd>Enter</kbd> new line</span>
109 </div>
110 <button id="send" class="send" type="submit" aria-label="Send message">↑</button>
111 </div>
112 <input id="pdf-input" type="file" accept="application/pdf,.pdf" multiple hidden>
113 </form>
114 <p class="privacy-note">Your map stays in this browser. Added PDFs are uploaded to the configured API.</p>
115 </div>
116 </section>
117
118 <aside class="map-panel" aria-label="Concept map">
119 <div class="map-heading">
120 <div>
121 <span class="eyebrow">Your exploration</span>
122 <h2>Concept map</h2>
123 </div>
124 <button id="collapse-map" class="icon-button" type="button" aria-label="Collapse concept map">→</button>
125 </div>
126 <p id="map-help" class="map-help">Select a node to revisit it and start a new branch.</p>
127 <div class="map-toolbar" aria-label="Concept map controls">
128 <button id="expand-all-nodes" type="button" title="Expand all branches">Expand</button>
129 <button id="collapse-all-nodes" type="button" title="Collapse all branches">Collapse</button>
130 <span class="map-toolbar-spacer"></span>
131 <button id="zoom-out-tree" type="button" aria-label="Zoom concept map out">−</button>
132 <output id="tree-zoom" aria-label="Concept map zoom">100%</output>
133 <button id="zoom-in-tree" type="button" aria-label="Zoom concept map in">+</button>
134 </div>
135 <nav id="tree" class="tree" aria-label="Question branches"></nav>
136 <div id="map-empty" class="map-empty">
137 <span class="empty-orbit">✦</span>
138 <p>Your questions will grow into a map here.</p>
139 </div>
140 <div class="map-stats">
141 <span><strong id="node-count">0</strong> nodes</span>
142 <span><strong id="branch-count">0</strong> branches</span>
143 </div>
144 </aside>
145 <button id="expand-map" class="expand-map" type="button" hidden>← Concept map</button>
146 </main>
147
148 <div id="selection-menu" class="selection-menu" role="dialog" aria-label="Ask about selected text" hidden>
149 <div class="selection-topline">
150 <span>Ask about selection</span>
151 <button id="close-selection" type="button" aria-label="Close">×</button>
152 </div>
153 <blockquote id="selection-preview"></blockquote>
154 <select id="selection-action" aria-label="Question type">
155 <option value="Explain this passage in simpler terms.">Explain simply</option>
156 <option value="What is">What is</option>
157 <option value="Go deeper into this idea and its implications.">Go deeper</option>
158 <option value="Give me a concrete example of this.">Give an example</option>
159 <option value="What assumptions does this passage make?">Challenge assumptions</option>
160 <option value="custom">Write my own…</option>
161 </select>
162 <textarea id="selection-custom" rows="2" placeholder="What would you like to ask?" hidden></textarea>
163 <button id="ask-selection" class="button button-dark" type="button">Ask</button>
164 </div>
165
166 <dialog id="settings-dialog" class="settings-dialog">
167 <form id="settings-form" method="dialog">
168 <div class="dialog-heading">
169 <div>
170 <span class="eyebrow">Connection</span>
171 <h2>API settings</h2>
172 </div>
173 <button id="close-settings" class="icon-button" type="button" aria-label="Close settings">×</button>
174 </div>
175 <p class="settings-copy">Use an OpenAI API key (not a ChatGPT password). It is stored only in this browser and sent directly to the configured API.</p>
176 <label>
177 API key
178 <div class="secret-field">
179 <input id="api-key" type="password" placeholder="sk-…" autocomplete="off" spellcheck="false">
180 <button id="toggle-key" type="button" aria-label="Show API key">Show</button>
181 </div>
182 </label>
183 <label>
184 Model
185 <input id="model" type="text" value="gpt-5-mini" placeholder="gpt-5-mini" spellcheck="false">
186 </label>
187 <details>
188 <summary>Advanced</summary>
189 <label>
190 Responses API endpoint
191 <input id="api-url" type="url" value="https://api.openai.com/v1/responses" spellcheck="false">
192 </label>
193 <label>
194 System instructions
195 <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>
196 </label>
197 </details>
198 <div class="security-callout"><strong>Local-use note</strong> — any script running on this origin can access local storage. Use a restricted project key and never deploy this frontend publicly with a saved key.</div>
199 <div class="dialog-actions">
200 <button id="clear-history" class="button button-ghost danger" type="button">Clear chat history</button>
201 <button id="forget-key" class="button button-ghost danger" type="button">Forget key</button>
202 <button class="button button-dark" value="save" type="submit">Save settings</button>
203 </div>
204 </form>
205 </dialog>
206
207 <dialog id="usage-dialog" class="settings-dialog usage-dialog">
208 <form method="dialog">
209 <div class="dialog-heading">
210 <div>
211 <span class="eyebrow">Local estimate</span>
212 <h2>API usage</h2>
213 </div>
214 <button id="close-usage" class="icon-button" type="button" aria-label="Close API usage">×</button>
215 </div>
216 <p class="settings-copy"><span id="usage-month"></span> · Requests made from this browser</p>
217
218 <div class="usage-summary">
219 <div class="usage-stat primary">
220 <span>Tokens</span>
221 <strong id="usage-total">0</strong>
222 </div>
223 <div class="usage-stat">
224 <span>Estimated cost</span>
225 <strong id="usage-cost">$0.00</strong>
226 </div>
227 <div class="usage-stat">
228 <span>Requests</span>
229 <strong id="usage-requests">0</strong>
230 </div>
231 </div>
232
233 <section class="usage-section" aria-labelledby="token-breakdown-title">
234 <h3 id="token-breakdown-title">Token breakdown</h3>
235 <div class="token-breakdown">
236 <span>Input <strong id="usage-input">0</strong></span>
237 <span>Cached input <strong id="usage-cached">0</strong></span>
238 <span>Output <strong id="usage-output">0</strong></span>
239 </div>
240 </section>
241
242 <section class="usage-section" aria-labelledby="models-title">
243 <h3 id="models-title">By model</h3>
244 <div id="usage-models" class="usage-models"></div>
245 </section>
246
247 <section class="usage-section" aria-labelledby="recent-title">
248 <h3 id="recent-title">Recent requests</h3>
249 <div id="usage-recent" class="usage-recent"></div>
250 </section>
251
252 <p id="usage-note" class="usage-note"></p>
253 <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>
254 <div class="dialog-actions">
255 <button id="reset-usage" class="button button-ghost danger" type="button">Reset local history</button>
256 <a class="button button-ghost usage-dashboard" href="https://platform.openai.com/usage" target="_blank" rel="noopener noreferrer">Official dashboard ↗</a>
257 <button class="button button-dark" value="close" type="submit">Done</button>
258 </div>
259 </form>
260 </dialog>
261
262 <dialog id="shortcuts-dialog" class="settings-dialog shortcuts-dialog">
263 <form method="dialog">
264 <div class="dialog-heading">
265 <div>
266 <span class="eyebrow">Navigation</span>
267 <h2>Keyboard shortcuts</h2>
268 </div>
269 <button id="close-shortcuts" class="icon-button" type="button" aria-label="Close keyboard shortcuts">×</button>
270 </div>
271 <div class="shortcut-list">
272 <span><kbd>j</kbd> / <kbd>k</kbd></span><span>Next / previous visible node</span>
273 <span><kbd>h</kbd> / <kbd>l</kbd></span><span>Parent / first child or expand</span>
274 <span><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></span><span>First / last visible node</span>
275 <span><kbd>J</kbd> / <kbd>K</kbd></span><span>Next / previous conversation</span>
276 <span><kbd>/</kbd></span><span>Search all conversations</span>
277 <span><kbd>i</kbd></span><span>Focus the question composer</span>
278 <span><kbd>Space</kbd></span><span>Collapse or expand the active branch</span>
279 <span><kbd>Esc</kbd></span><span>Leave search or input mode</span>
280 <span><kbd>?</kbd></span><span>Show this shortcut reference</span>
281 </div>
282 <div class="dialog-actions"><button class="button button-dark" value="close" type="submit">Done</button></div>
283 </form>
284 </dialog>
285
286 <div id="toast" class="toast" role="status" aria-live="polite"></div>
287
288 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
289 <script defer src="app.js"></script>
290</body>
291</html>