TheHickman commited on
Commit
f83b893
·
verified ·
1 Parent(s): 4bfa0a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +231 -373
app.py CHANGED
@@ -1,376 +1,234 @@
1
- import { useState, useRef, useCallback } from "react";
2
-
3
- const CONTENT = `Text generation is the task of producing natural language text given an input prompt. It is commonly used for chatbots, creative writing, summarization, and code generation.
4
-
5
- Most modern text generation models are based on the transformer architecture and are trained using next-token prediction. The transformer uses self-attention mechanisms to weigh the importance of different words in a sequence when making predictions.
6
-
7
- During inference, the model repeatedly samples the most likely next token until a stopping condition is reached. This process is called autoregressive generation. Parameters like temperature and top-p sampling control the randomness and diversity of the output.
8
-
9
- Large language models (LLMs) like GPT-4, Claude, and Llama are trained on vast corpora of text from the internet, books, and other sources. This gives them broad world knowledge and language understanding.
10
-
11
- Instruction-tuned models are further trained to follow user instructions, using techniques like supervised fine-tuning (SFT) and reinforcement learning from human feedback (RLHF). This makes them much more useful as assistants compared to base language models.`;
12
-
13
- const PARAGRAPHS = CONTENT.split("\n\n");
14
-
15
- export default function App() {
16
- const [segments, setSegments] = useState(() =>
17
- PARAGRAPHS.map((text, i) => ({
18
- id: i,
19
- parts: [{ type: "text", content: text }],
20
- }))
21
- );
22
- const [selection, setSelection] = useState(null);
23
- const [loading, setLoading] = useState(false);
24
- const [buttonPos, setButtonPos] = useState(null);
25
- const contentRef = useRef(null);
26
-
27
- const handleMouseUp = useCallback(() => {
28
- const sel = window.getSelection();
29
- if (!sel || sel.isCollapsed) {
30
- setSelection(null);
31
- setButtonPos(null);
32
- return;
33
- }
34
- const text = sel.toString().trim();
35
- if (!text || text.length < 3) {
36
- setSelection(null);
37
- setButtonPos(null);
38
- return;
39
- }
40
-
41
- // Find which paragraph this belongs to
42
- let paraIndex = -1;
43
- let node = sel.anchorNode;
44
- while (node && node !== contentRef.current) {
45
- if (node.dataset && node.dataset.para !== undefined) {
46
- paraIndex = parseInt(node.dataset.para);
47
- break;
48
- }
49
- node = node.parentNode;
50
- }
51
-
52
- if (paraIndex === -1) {
53
- setSelection(null);
54
- setButtonPos(null);
55
- return;
56
- }
57
-
58
- const range = sel.getRangeAt(0);
59
- const rect = range.getBoundingClientRect();
60
- const containerRect = contentRef.current.getBoundingClientRect();
61
-
62
- setSelection({ text, paraIndex });
63
- setButtonPos({
64
- top: rect.bottom - containerRect.top + 8,
65
- left: Math.min(
66
- rect.left - containerRect.left,
67
- containerRect.width - 160
68
- ),
69
- });
70
- }, []);
71
-
72
- const handleExplain = async () => {
73
- if (!selection || loading) return;
74
- const { text, paraIndex } = selection;
75
- setLoading(true);
76
- setButtonPos(null);
77
-
78
- try {
79
- const response = await fetch("https://api.anthropic.com/v1/messages", {
80
- method: "POST",
81
- headers: { "Content-Type": "application/json" },
82
- body: JSON.stringify({
83
- model: "claude-sonnet-4-20250514",
84
- max_tokens: 1000,
85
- messages: [
86
- {
87
- role: "user",
88
- content: `You are an ML instructor. A student highlighted this text from a learning resource and wants a clear, concise explanation (2-4 sentences, no fluff):\n\n"${text}"\n\nExplain it simply and intuitively.`,
89
- },
90
- ],
91
- }),
92
- });
93
- const data = await response.json();
94
- const explanation = data.content?.[0]?.text || "Could not generate explanation.";
95
-
96
- // Replace the selected text in the paragraph with [original, explanation]
97
- setSegments((prev) =>
98
- prev.map((para) => {
99
- if (para.id !== paraIndex) return para;
100
- const newParts = [];
101
- for (const part of para.parts) {
102
- if (part.type !== "text") {
103
- newParts.push(part);
104
- continue;
105
- }
106
- const idx = part.content.indexOf(text);
107
- if (idx === -1) {
108
- newParts.push(part);
109
- continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
- if (idx > 0) newParts.push({ type: "text", content: part.content.slice(0, idx) });
112
- newParts.push({ type: "explained", original: text, explanation });
113
- const after = part.content.slice(idx + text.length);
114
- if (after) newParts.push({ type: "text", content: after });
115
- }
116
- return { ...para, parts: newParts };
117
- })
118
- );
119
- } catch (e) {
120
- console.error(e);
121
- }
122
-
123
- setSelection(null);
124
- setLoading(false);
125
- window.getSelection()?.removeAllRanges();
126
- };
127
-
128
- const dismissExplain = (paraId, partIdx) => {
129
- setSegments((prev) =>
130
- prev.map((para) => {
131
- if (para.id !== paraId) return para;
132
- const newParts = [...para.parts];
133
- const part = newParts[partIdx];
134
- if (part.type === "explained") {
135
- newParts.splice(partIdx, 1, { type: "text", content: part.original });
136
- }
137
- // Merge adjacent text parts
138
- const merged = [];
139
- for (const p of newParts) {
140
- if (p.type === "text" && merged.length && merged[merged.length - 1].type === "text") {
141
- merged[merged.length - 1] = { type: "text", content: merged[merged.length - 1].content + p.content };
142
- } else {
143
- merged.push(p);
144
- }
145
- }
146
- return { ...para, parts: merged };
147
- })
148
- );
149
- };
150
-
151
- return (
152
- <div style={{ fontFamily: "'Georgia', serif", background: "#faf8f4", minHeight: "100vh", padding: "0" }}>
153
- <style>{`
154
- @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;1,8..60,300&display=swap');
155
-
156
- * { box-sizing: border-box; }
157
-
158
- .reader-wrap {
159
- max-width: 680px;
160
- margin: 0 auto;
161
- padding: 60px 32px 120px;
162
- }
163
-
164
- h1.title {
165
- font-family: 'Playfair Display', Georgia, serif;
166
- font-size: 2.4rem;
167
- font-weight: 600;
168
- color: #1a1208;
169
- margin: 0 0 8px;
170
- letter-spacing: -0.5px;
171
- line-height: 1.15;
172
- }
173
-
174
- .subtitle {
175
- font-family: 'Source Serif 4', Georgia, serif;
176
- font-size: 0.9rem;
177
- color: #a09070;
178
- margin: 0 0 48px;
179
- letter-spacing: 0.05em;
180
- text-transform: uppercase;
181
  }
182
-
183
- .hint {
184
- font-family: 'Source Serif 4', Georgia, serif;
185
- font-size: 0.85rem;
186
- color: #b0a080;
187
- margin: 0 0 36px;
188
- display: flex;
189
- align-items: center;
190
- gap: 8px;
191
- }
192
-
193
- .hint-icon {
194
- width: 18px;
195
- height: 18px;
196
- background: #e8dfc8;
197
- border-radius: 50%;
198
- display: inline-flex;
199
- align-items: center;
200
- justify-content: center;
201
- font-size: 10px;
202
- flex-shrink: 0;
203
- }
204
-
205
- .para {
206
- font-family: 'Source Serif 4', Georgia, serif;
207
- font-size: 1.08rem;
208
- line-height: 1.85;
209
- color: #2c2010;
210
- margin-bottom: 1.6em;
211
- position: relative;
212
- }
213
-
214
- .explained-bubble {
215
- display: inline;
216
- position: relative;
217
- }
218
-
219
- .explained-original {
220
- background: linear-gradient(120deg, #f5e6a3 0%, #f0d870 100%);
221
- border-radius: 3px;
222
- padding: 1px 3px;
223
- text-decoration: line-through;
224
- text-decoration-color: #c8a800;
225
- color: #6b5a10;
226
- font-style: italic;
227
- opacity: 0.7;
228
- font-size: 0.88em;
229
- cursor: pointer;
230
- }
231
-
232
- .explained-text {
233
- display: inline-block;
234
- background: linear-gradient(135deg, #fffbee 0%, #fff8e0 100%);
235
- border-left: 3px solid #d4a800;
236
- border-radius: 0 6px 6px 0;
237
- padding: 6px 12px 6px 12px;
238
- margin: 4px 0;
239
- color: #3a2e08;
240
- font-size: 0.95rem;
241
- line-height: 1.65;
242
- box-shadow: 0 2px 8px rgba(180,140,0,0.1);
243
- position: relative;
244
- }
245
-
246
- .dismiss-btn {
247
- position: absolute;
248
- top: 4px;
249
- right: 6px;
250
- background: none;
251
- border: none;
252
- color: #c8a800;
253
- cursor: pointer;
254
- font-size: 14px;
255
- padding: 0 2px;
256
- opacity: 0.6;
257
- line-height: 1;
258
- transition: opacity 0.15s;
259
- }
260
- .dismiss-btn:hover { opacity: 1; }
261
-
262
- .float-btn {
263
- position: absolute;
264
- z-index: 100;
265
- background: #1a1208;
266
- color: #f5e8c0;
267
- border: none;
268
- border-radius: 6px;
269
- padding: 8px 16px;
270
- font-family: 'Source Serif 4', Georgia, serif;
271
- font-size: 0.82rem;
272
- letter-spacing: 0.04em;
273
- cursor: pointer;
274
- box-shadow: 0 4px 16px rgba(0,0,0,0.25);
275
- display: flex;
276
- align-items: center;
277
- gap: 6px;
278
- transition: background 0.15s, transform 0.1s;
279
- white-space: nowrap;
280
- }
281
- .float-btn:hover { background: #2e2010; transform: translateY(-1px); }
282
- .float-btn:active { transform: translateY(0); }
283
-
284
- .loading-overlay {
285
- position: fixed;
286
- bottom: 28px;
287
- left: 50%;
288
- transform: translateX(-50%);
289
- background: #1a1208;
290
- color: #f5e8c0;
291
- border-radius: 24px;
292
- padding: 10px 22px;
293
- font-family: 'Source Serif 4', Georgia, serif;
294
- font-size: 0.85rem;
295
- display: flex;
296
- align-items: center;
297
- gap: 10px;
298
- box-shadow: 0 4px 20px rgba(0,0,0,0.3);
299
- }
300
-
301
- .spinner {
302
- width: 14px;
303
- height: 14px;
304
- border: 2px solid rgba(245,232,192,0.3);
305
- border-top-color: #f5e8c0;
306
- border-radius: 50%;
307
- animation: spin 0.7s linear infinite;
308
- }
309
- @keyframes spin { to { transform: rotate(360deg); } }
310
-
311
- .divider {
312
- border: none;
313
- border-top: 1px solid #e0d4b8;
314
- margin: 40px 0;
315
- }
316
- `}</style>
317
-
318
- <div className="reader-wrap">
319
- <h1 className="title">Text Generation</h1>
320
- <p className="subtitle">Machine Learning · Reading Guide</p>
321
-
322
- <div className="hint">
323
- <span className="hint-icon">✦</span>
324
- Highlight any text, then click <em style={{ fontStyle: "italic", color: "#8a7450" }}>&nbsp;Explain&nbsp;</em> to replace it with an AI explanation.
325
- </div>
326
-
327
- <hr className="divider" />
328
-
329
- <div ref={contentRef} style={{ position: "relative" }} onMouseUp={handleMouseUp}>
330
- {segments.map((para) => (
331
- <p key={para.id} className="para" data-para={para.id}>
332
- {para.parts.map((part, pi) =>
333
- part.type === "text" ? (
334
- <span key={pi} data-para={para.id}>{part.content}</span>
335
- ) : (
336
- <span key={pi} className="explained-bubble">
337
- {" "}
338
- <span className="explained-text">
339
- <button className="dismiss-btn" onClick={() => dismissExplain(para.id, pi)} title="Restore original">✕</button>
340
- {part.explanation}
341
- </span>{" "}
342
- <span
343
- className="explained-original"
344
- title="Click to restore"
345
- onClick={() => dismissExplain(para.id, pi)}
346
- >
347
- {part.original}
348
- </span>
349
- </span>
350
- )
351
- )}
352
- </p>
353
- ))}
354
-
355
- {buttonPos && !loading && (
356
- <button
357
- className="float-btn"
358
- style={{ top: buttonPos.top, left: buttonPos.left }}
359
- onMouseDown={(e) => e.preventDefault()}
360
- onClick={handleExplain}
361
- >
362
- ✦ Explain
363
- </button>
364
- )}
365
- </div>
366
- </div>
367
-
368
- {loading && (
369
- <div className="loading-overlay">
370
- <div className="spinner" />
371
- Generating explanation…
372
- </div>
373
- )}
374
  </div>
375
- );
376
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from anthropic import Anthropic
3
+
4
+ client = Anthropic()
5
+
6
+ CONTENT_PARAGRAPHS = [
7
+ "Text generation is the task of producing natural language text given an input prompt. It is commonly used for chatbots, creative writing, summarization, and code generation.",
8
+ "Most modern text generation models are based on the transformer architecture and are trained using next-token prediction. The transformer uses self-attention mechanisms to weigh the importance of different words in a sequence when making predictions.",
9
+ "During inference, the model repeatedly samples the most likely next token until a stopping condition is reached. This process is called autoregressive generation. Parameters like temperature and top-p sampling control the randomness and diversity of the output.",
10
+ "Large language models (LLMs) like GPT-4, Claude, and Llama are trained on vast corpora of text from the internet, books, and other sources. This gives them broad world knowledge and language understanding.",
11
+ "Instruction-tuned models are further trained to follow user instructions, using techniques like supervised fine-tuning (SFT) and reinforcement learning from human feedback (RLHF). This makes them much more useful as assistants compared to base language models.",
12
+ ]
13
+
14
+ INITIAL_HTML = "\n".join(
15
+ f'<p class="content-para" id="para-{i}">{p}</p>'
16
+ for i, p in enumerate(CONTENT_PARAGRAPHS)
17
+ )
18
+
19
+ def explain_text(selected_text, current_html):
20
+ if not selected_text or not selected_text.strip():
21
+ return current_html, "⚠️ Please select some text first."
22
+
23
+ selected_text = selected_text.strip()
24
+
25
+ try:
26
+ response = client.messages.create(
27
+ model="claude-opus-4-5",
28
+ max_tokens=300,
29
+ messages=[
30
+ {
31
+ "role": "user",
32
+ "content": f"You are an ML instructor. Explain this text from a learning resource in 2-3 clear, simple sentences for a beginner:\n\n\"{selected_text}\"",
33
+ }
34
+ ],
35
+ )
36
+ explanation = response.content[0].text.strip()
37
+ except Exception as e:
38
+ return current_html, f"❌ Error: {str(e)}"
39
+
40
+ # Replace the selected text in the HTML with explanation block
41
+ escaped = selected_text.replace('"', '&quot;')
42
+ replacement = (
43
+ f'<mark class="explained-original" title="Original text">{selected_text}</mark>'
44
+ f'<span class="explanation-block">💡 {explanation}</span>'
45
+ )
46
+
47
+ new_html = current_html.replace(selected_text, replacement, 1)
48
+
49
+ if new_html == current_html:
50
+ return current_html, "⚠️ Could not find selected text in the document. Try selecting again."
51
+
52
+ return new_html, f"✅ Explained: \"{selected_text[:60]}{'...' if len(selected_text) > 60 else ''}\""
53
+
54
+
55
+ def reset_content():
56
+ return INITIAL_HTML, "", "Document reset."
57
+
58
+
59
+ CSS = """
60
+ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;1,8..60,300&display=swap');
61
+
62
+ body { background: #faf8f4 !important; }
63
+
64
+ #reader-content {
65
+ font-family: 'Source Serif 4', Georgia, serif;
66
+ font-size: 1.05rem;
67
+ line-height: 1.85;
68
+ color: #2c2010;
69
+ background: #faf8f4;
70
+ padding: 28px 32px;
71
+ border-radius: 10px;
72
+ border: 1px solid #e8dfc8;
73
+ min-height: 320px;
74
+ }
75
+
76
+ #reader-content h1 {
77
+ font-family: 'Playfair Display', Georgia, serif;
78
+ font-size: 2rem;
79
+ color: #1a1208;
80
+ margin-bottom: 4px;
81
+ }
82
+
83
+ .content-para {
84
+ margin-bottom: 1.4em;
85
+ }
86
+
87
+ mark.explained-original {
88
+ background: linear-gradient(120deg, #f5e6a3, #f0d870);
89
+ border-radius: 3px;
90
+ padding: 1px 3px;
91
+ text-decoration: line-through;
92
+ text-decoration-color: #c8a800;
93
+ color: #6b5a10;
94
+ font-style: italic;
95
+ opacity: 0.75;
96
+ }
97
+
98
+ .explanation-block {
99
+ display: inline-block;
100
+ background: #fffbee;
101
+ border-left: 3px solid #d4a800;
102
+ border-radius: 0 6px 6px 0;
103
+ padding: 6px 12px;
104
+ margin: 4px 2px;
105
+ color: #3a2e08;
106
+ font-size: 0.93rem;
107
+ line-height: 1.6;
108
+ box-shadow: 0 2px 8px rgba(180,140,0,0.1);
109
+ }
110
+
111
+ #hint-text {
112
+ font-family: 'Source Serif 4', Georgia, serif;
113
+ color: #a09070;
114
+ font-size: 0.88rem;
115
+ margin-bottom: 8px;
116
+ }
117
+
118
+ .selected-box textarea {
119
+ font-family: 'Source Serif 4', Georgia, serif !important;
120
+ font-size: 0.95rem !important;
121
+ color: #3a2e08 !important;
122
+ background: #fffbee !important;
123
+ border: 1px solid #d4c88a !important;
124
+ border-radius: 8px !important;
125
+ }
126
+
127
+ .explain-btn {
128
+ background: #1a1208 !important;
129
+ color: #f5e8c0 !important;
130
+ border-radius: 8px !important;
131
+ font-family: 'Source Serif 4', Georgia, serif !important;
132
+ font-size: 0.95rem !important;
133
+ border: none !important;
134
+ }
135
+ .explain-btn:hover {
136
+ background: #2e2010 !important;
137
+ }
138
+
139
+ .reset-btn {
140
+ background: transparent !important;
141
+ color: #a09070 !important;
142
+ border: 1px solid #d4c88a !important;
143
+ border-radius: 8px !important;
144
+ font-family: 'Source Serif 4', Georgia, serif !important;
145
+ font-size: 0.9rem !important;
146
+ }
147
+
148
+ .status-text {
149
+ font-family: 'Source Serif 4', Georgia, serif !important;
150
+ color: #6b5a10 !important;
151
+ font-size: 0.85rem !important;
152
+ }
153
+ """
154
+
155
+ JS_CAPTURE_SELECTION = """
156
+ function setupSelectionCapture() {
157
+ document.addEventListener("mouseup", function() {
158
+ const selection = window.getSelection().toString().trim();
159
+ if (selection.length > 2) {
160
+ const textarea = document.querySelector('.selected-box textarea');
161
+ if (textarea) {
162
+ const nativeSetter = Object.getOwnPropertyDescriptor(
163
+ window.HTMLTextAreaElement.prototype, "value"
164
+ ).set;
165
+ nativeSetter.call(textarea, selection);
166
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
167
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
+ });
170
+ }
171
+ setupSelectionCapture();
172
+ """
173
+
174
+ with gr.Blocks(css=CSS, js=JS_CAPTURE_SELECTION, title="ML Reader — Highlight & Explain") as demo:
175
+
176
+ gr.HTML("""
177
+ <div style="max-width: 720px; margin: 0 auto; padding: 32px 16px 0;">
178
+ <h1 style="font-family:'Playfair Display',Georgia,serif; font-size:2.1rem; color:#1a1208; margin-bottom:4px;">
179
+ Text Generation
180
+ </h1>
181
+ <p style="font-family:'Source Serif 4',Georgia,serif; font-size:0.82rem; color:#a09070; letter-spacing:0.08em; text-transform:uppercase; margin:0 0 12px;">
182
+ Machine Learning · Reading Guide
183
+ </p>
184
+ <p id="hint-text" style="font-family:'Source Serif 4',Georgia,serif; color:#a09070; font-size:0.88rem; margin-bottom:20px;">
185
+ Highlight any text below, then click <em>Explain Selection</em> to replace it with an AI explanation.
186
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  </div>
188
+ """)
189
+
190
+ with gr.Column(elem_style="max-width:720px; margin:0 auto; padding:0 16px 60px;"):
191
+
192
+ content_html = gr.HTML(
193
+ value=f'<div id="reader-content">{INITIAL_HTML}</div>',
194
+ label="",
195
+ )
196
+
197
+ selected_text = gr.Textbox(
198
+ label="Selected text",
199
+ placeholder="Highlight text above — it will appear here automatically…",
200
+ lines=2,
201
+ elem_classes=["selected-box"],
202
+ show_label=True,
203
+ )
204
+
205
+ with gr.Row():
206
+ explain_btn = gr.Button("✦ Explain Selection", elem_classes=["explain-btn"], variant="primary")
207
+ reset_btn = gr.Button("↺ Reset", elem_classes=["reset-btn"])
208
+
209
+ status = gr.Textbox(label="", interactive=False, show_label=False, elem_classes=["status-text"])
210
+
211
+ # Hidden state to track current HTML
212
+ html_state = gr.State(f'<div id="reader-content">{INITIAL_HTML}</div>')
213
+
214
+ explain_btn.click(
215
+ fn=explain_text,
216
+ inputs=[selected_text, html_state],
217
+ outputs=[html_state, status],
218
+ ).then(
219
+ fn=lambda h: h,
220
+ inputs=[html_state],
221
+ outputs=[content_html],
222
+ )
223
+
224
+ reset_btn.click(
225
+ fn=reset_content,
226
+ inputs=[],
227
+ outputs=[html_state, selected_text, status],
228
+ ).then(
229
+ fn=lambda h: f'<div id="reader-content">{INITIAL_HTML}</div>',
230
+ inputs=[html_state],
231
+ outputs=[content_html],
232
+ )
233
+
234
+ demo.launch()