wjbmattingly commited on
Commit
3cbfea9
·
verified ·
1 Parent(s): 6f822bb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1804 -18
index.html CHANGED
@@ -1,19 +1,1805 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <!--
4
+ Latin Vulgate Text Editor with Marginalia
5
+ This standalone HTML file provides a sophisticated text editor for Latin texts with:
6
+ - Custom text input - paste your own Latin text for annotation
7
+ - Interactive text selection and semantic search
8
+ - Marginalia annotations linked to similar passages
9
+ - TEI XML export functionality
10
+ - Integration with Gradio app at https://medieval-data-latin-vulgate.hf.space
11
+
12
+ To use:
13
+ 1. Serve this file from a web server (run: python serve.py)
14
+ 2. Or deploy to any web hosting platform
15
+ 3. Paste your own text or use the provided sample
16
+ 4. Select text to find similar biblical passages
17
+ 5. Export your annotated text as TEI XML
18
+
19
+ Dependencies: None - completely self-contained HTML/CSS/JavaScript
20
+ -->
21
+ <head>
22
+ <meta charset="UTF-8">
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
+ <title>Latin Vulgate Text Editor with Marginalia</title>
25
+ <style>
26
+ * {
27
+ margin: 0;
28
+ padding: 0;
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ body {
33
+ font-family: 'Georgia', serif;
34
+ line-height: 1.6;
35
+ background-color: #f8f7f5;
36
+ color: #333;
37
+ }
38
+
39
+ .container {
40
+ display: grid;
41
+ grid-template-columns: 1fr 250px;
42
+ min-height: 100vh;
43
+ padding: 20px;
44
+ max-width: 1200px;
45
+ margin: 0 auto;
46
+ gap: 20px;
47
+ }
48
+
49
+ .main-content {
50
+ background: white;
51
+ padding: 40px;
52
+ border-radius: 8px;
53
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
54
+ overflow-y: auto;
55
+ position: relative;
56
+ }
57
+
58
+ .margin-area {
59
+ background: #f8f7f5;
60
+ border: 1px solid #e5e5e5;
61
+ border-radius: 8px;
62
+ padding: 20px;
63
+ position: sticky;
64
+ top: 20px;
65
+ height: fit-content;
66
+ max-height: calc(100vh - 40px);
67
+ overflow-y: auto;
68
+ }
69
+
70
+ .margin-area h3 {
71
+ margin: 0 0 15px 0;
72
+ color: #2c3e50;
73
+ font-size: 1.1em;
74
+ border-bottom: 1px solid #ddd;
75
+ padding-bottom: 10px;
76
+ }
77
+
78
+ .margin-note {
79
+ background: white;
80
+ border: 1px solid #ddd;
81
+ border-left: 3px solid #3498db;
82
+ border-radius: 4px;
83
+ padding: 10px;
84
+ margin-bottom: 10px;
85
+ font-size: 0.85em;
86
+ position: relative;
87
+ cursor: pointer;
88
+ }
89
+
90
+ .margin-note:hover {
91
+ background: #f8f9fa;
92
+ border-left-color: #2980b9;
93
+ }
94
+
95
+ .margin-note-reference {
96
+ font-weight: bold;
97
+ color: #2c3e50;
98
+ margin-bottom: 5px;
99
+ }
100
+
101
+ .margin-note-text {
102
+ color: #555;
103
+ line-height: 1.3;
104
+ margin-bottom: 5px;
105
+ }
106
+
107
+ .margin-note-similarity {
108
+ font-size: 0.75em;
109
+ color: #777;
110
+ margin-bottom: 5px;
111
+ }
112
+
113
+ .margin-note-remove {
114
+ position: absolute;
115
+ top: 5px;
116
+ right: 5px;
117
+ background: #e74c3c;
118
+ color: white;
119
+ border: none;
120
+ border-radius: 50%;
121
+ width: 18px;
122
+ height: 18px;
123
+ font-size: 10px;
124
+ cursor: pointer;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ }
129
+
130
+ .margin-note-remove:hover {
131
+ background: #c0392b;
132
+ }
133
+
134
+ .margin-note.highlighted {
135
+ background: #e8f4fd;
136
+ border-left-color: #3498db;
137
+ box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
138
+ }
139
+
140
+ .sidebar {
141
+ display: none; /* Hidden - marginalia now inline */
142
+ }
143
+
144
+ .header {
145
+ text-align: center;
146
+ margin-bottom: 30px;
147
+ padding-bottom: 20px;
148
+ border-bottom: 2px solid #eee;
149
+ }
150
+
151
+ .header h1 {
152
+ color: #2c3e50;
153
+ font-size: 2.5em;
154
+ margin-bottom: 10px;
155
+ }
156
+
157
+ .controls {
158
+ display: flex;
159
+ gap: 15px;
160
+ margin-bottom: 20px;
161
+ align-items: center;
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .control-group {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 5px;
169
+ }
170
+
171
+ label {
172
+ font-weight: bold;
173
+ font-size: 0.9em;
174
+ color: #555;
175
+ }
176
+
177
+ select, input, button {
178
+ padding: 8px 12px;
179
+ border: 1px solid #ddd;
180
+ border-radius: 4px;
181
+ font-size: 14px;
182
+ }
183
+
184
+ textarea#custom-text {
185
+ border: 1px solid #ddd;
186
+ border-radius: 4px;
187
+ padding: 10px;
188
+ font-family: 'Georgia', serif;
189
+ font-size: 14px;
190
+ line-height: 1.4;
191
+ outline: none;
192
+ transition: border-color 0.3s;
193
+ resize: vertical;
194
+ }
195
+
196
+ textarea#custom-text:focus {
197
+ border-color: #3498db;
198
+ box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
199
+ }
200
+
201
+ button {
202
+ background: #3498db;
203
+ color: white;
204
+ cursor: pointer;
205
+ border: none;
206
+ transition: background-color 0.3s;
207
+ }
208
+
209
+ button:hover {
210
+ background: #2980b9;
211
+ }
212
+
213
+ .export-btn {
214
+ background: #27ae60;
215
+ margin-left: auto;
216
+ }
217
+
218
+ .export-btn:hover {
219
+ background: #219a52;
220
+ }
221
+
222
+ .text-content {
223
+ font-size: 1.1em;
224
+ line-height: 1.8;
225
+ text-align: justify;
226
+ user-select: text;
227
+ cursor: text;
228
+ }
229
+
230
+ .text-content p {
231
+ margin-bottom: 20px;
232
+ text-indent: 2em;
233
+ }
234
+
235
+ .selected-text {
236
+ background-color: #ffffcc;
237
+ padding: 2px 4px;
238
+ border-radius: 3px;
239
+ position: relative;
240
+ }
241
+
242
+ .marginalia-marker {
243
+ background-color: #e8f4fd;
244
+ border-left: 3px solid #3498db;
245
+ padding: 2px 4px;
246
+ border-radius: 3px;
247
+ position: relative;
248
+ cursor: pointer;
249
+ }
250
+
251
+ .marginalia-indicator {
252
+ position: absolute;
253
+ top: -8px;
254
+ right: -8px;
255
+ background: #3498db;
256
+ color: white;
257
+ border-radius: 50%;
258
+ width: 18px;
259
+ height: 18px;
260
+ font-size: 10px;
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ font-weight: bold;
265
+ z-index: 10;
266
+ }
267
+
268
+ .marginalia-indicator.multiple {
269
+ background: #e74c3c;
270
+ }
271
+
272
+ .marginalia-tooltip {
273
+ position: absolute;
274
+ background: white;
275
+ border: 1px solid #ddd;
276
+ border-radius: 6px;
277
+ padding: 12px;
278
+ box-shadow: 0 4px 15px rgba(0,0,0,0.15);
279
+ z-index: 1000;
280
+ max-width: 400px;
281
+ min-width: 300px;
282
+ display: none;
283
+ top: 100%;
284
+ left: 0;
285
+ margin-top: 5px;
286
+ font-size: 0.9em;
287
+ }
288
+
289
+ .marginalia-tooltip.show {
290
+ display: block;
291
+ }
292
+
293
+ .marginalia-tooltip-item {
294
+ margin-bottom: 12px;
295
+ padding-bottom: 12px;
296
+ border-bottom: 1px solid #eee;
297
+ }
298
+
299
+ .marginalia-tooltip-item:last-child {
300
+ margin-bottom: 0;
301
+ padding-bottom: 0;
302
+ border-bottom: none;
303
+ }
304
+
305
+ .marginalia-tooltip-reference {
306
+ font-weight: bold;
307
+ color: #2c3e50;
308
+ margin-bottom: 6px;
309
+ font-size: 0.95em;
310
+ }
311
+
312
+ .marginalia-tooltip-text {
313
+ font-size: 0.9em;
314
+ line-height: 1.4;
315
+ margin-bottom: 6px;
316
+ color: #444;
317
+ }
318
+
319
+ .marginalia-tooltip-similarity {
320
+ font-size: 0.8em;
321
+ color: #666;
322
+ }
323
+
324
+
325
+
326
+ .popup {
327
+ position: fixed;
328
+ top: 50%;
329
+ left: 50%;
330
+ transform: translate(-50%, -50%);
331
+ background: white;
332
+ padding: 30px;
333
+ border-radius: 12px;
334
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
335
+ z-index: 1000;
336
+ max-width: 90%;
337
+ max-height: 80%;
338
+ overflow-y: auto;
339
+ display: none;
340
+ }
341
+
342
+ .popup-overlay {
343
+ position: fixed;
344
+ top: 0;
345
+ left: 0;
346
+ width: 100%;
347
+ height: 100%;
348
+ background: rgba(0,0,0,0.5);
349
+ z-index: 999;
350
+ display: none;
351
+ }
352
+
353
+ .popup-header {
354
+ display: flex;
355
+ justify-content: space-between;
356
+ align-items: center;
357
+ margin-bottom: 20px;
358
+ padding-bottom: 10px;
359
+ border-bottom: 1px solid #eee;
360
+ }
361
+
362
+ .popup-header h3 {
363
+ margin: 0;
364
+ flex-shrink: 0;
365
+ }
366
+
367
+ .popup-header > div {
368
+ display: flex;
369
+ align-items: center;
370
+ flex-wrap: wrap;
371
+ gap: 10px;
372
+ }
373
+
374
+ .close-btn {
375
+ background: #e74c3c;
376
+ color: white;
377
+ border: none;
378
+ padding: 8px 12px;
379
+ border-radius: 4px;
380
+ cursor: pointer;
381
+ }
382
+
383
+ .search-results {
384
+ max-height: 400px;
385
+ overflow-y: auto;
386
+ }
387
+
388
+ .result-item {
389
+ padding: 15px;
390
+ border: 1px solid #eee;
391
+ margin-bottom: 10px;
392
+ border-radius: 6px;
393
+ cursor: pointer;
394
+ transition: background-color 0.2s;
395
+ }
396
+
397
+ .result-item:hover {
398
+ background-color: #f8f9fa;
399
+ }
400
+
401
+ .result-reference {
402
+ font-weight: bold;
403
+ color: #2c3e50;
404
+ margin-bottom: 5px;
405
+ }
406
+
407
+ .result-text {
408
+ font-style: italic;
409
+ margin-bottom: 5px;
410
+ }
411
+
412
+ .result-similarity {
413
+ font-size: 0.9em;
414
+ color: #666;
415
+ }
416
+
417
+
418
+
419
+ .loading {
420
+ text-align: center;
421
+ padding: 20px;
422
+ color: #666;
423
+ }
424
+
425
+ .error {
426
+ color: #e74c3c;
427
+ background: #fdf2f2;
428
+ padding: 10px;
429
+ border-radius: 4px;
430
+ margin: 10px 0;
431
+ }
432
+
433
+ @media (max-width: 768px) {
434
+ .container {
435
+ padding: 10px;
436
+ grid-template-columns: 1fr; /* Stack on small screens */
437
+ gap: 10px;
438
+ }
439
+
440
+ .main-content {
441
+ padding: 20px;
442
+ }
443
+
444
+ .margin-area {
445
+ order: -1; /* Move marginalia above text on mobile */
446
+ position: static;
447
+ max-height: 300px;
448
+ }
449
+
450
+ .controls {
451
+ flex-direction: column;
452
+ align-items: stretch;
453
+ }
454
+
455
+ .control-group {
456
+ width: 100%;
457
+ }
458
+
459
+ #custom-text {
460
+ min-width: auto !important;
461
+ width: 100% !important;
462
+ font-size: 16px; /* Prevent zoom on iOS */
463
+ border: 1px solid #ddd;
464
+ border-radius: 4px;
465
+ outline: none;
466
+ transition: border-color 0.3s;
467
+ }
468
+
469
+ #custom-text:focus {
470
+ border-color: #3498db;
471
+ box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
472
+ }
473
+
474
+ .popup {
475
+ padding: 20px;
476
+ max-width: 95%;
477
+ }
478
+
479
+ .marginalia-tooltip {
480
+ max-width: 320px;
481
+ min-width: 280px;
482
+ font-size: 0.85em;
483
+ }
484
+
485
+ .margin-note {
486
+ font-size: 0.8em;
487
+ padding: 8px;
488
+ }
489
+ }
490
+ </style>
491
+ </head>
492
+ <body>
493
+ <div class="container">
494
+ <div class="main-content">
495
+ <div class="header">
496
+ <h1>Latin Vulgate Quote Finder</h1>
497
+ <p>Paste your own text or use the sample below. Select text to search for similar passages and add marginalia</p>
498
+
499
+ <div id="api-notice" style="background: #d4edda; color: #155724; padding: 15px; border-radius: 4px; margin: 10px 0; font-size: 0.9em; border-left: 4px solid #28a745;">
500
+ <strong>✅ API Configuration Fixed</strong><br>
501
+ The correct API endpoints have been identified. Your Gradio Space should now work properly with this interface.
502
+
503
+ <div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;">
504
+ <strong>Technical Details:</strong><br>
505
+ • Using proper Gradio API pattern: POST → GET with event streaming<br>
506
+ • Endpoint: <code>/gradio_api/call/predict</code><br>
507
+ • Your app.py has the correct <code>api_name="predict"</code> configuration
508
+ </div>
509
+
510
+ <div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;">
511
+ <strong>If you still see connection issues:</strong><br>
512
+ Try refreshing this page or restarting your <a href="https://huggingface.co/spaces/medieval-data/latin-vulgate" target="_blank" style="color: #1d4ed8;">HuggingFace Space</a>
513
+ </div>
514
+ </div>
515
+
516
+ <div id="connection-status" style="margin-top: 10px; padding: 8px; border-radius: 4px; font-size: 0.9em;">
517
+ <span id="status-indicator">🔄</span> <span id="status-text">Connecting to search backend...</span>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="controls">
522
+ <div class="control-group">
523
+ <label for="custom-text">Paste Your Text:</label>
524
+ <textarea id="custom-text" placeholder="Paste your Latin text here to annotate it..." rows="4" style="width: 100%; min-width: 300px; resize: vertical; font-family: 'Georgia', serif; line-height: 1.4; padding: 10px;"></textarea>
525
+ <div style="display: flex; gap: 10px; margin-top: 5px;">
526
+ <button onclick="loadCustomText()" style="background: #27ae60;">Load Text</button>
527
+ <button onclick="resetToSample()" style="background: #95a5a6;">Reset to Sample</button>
528
+ <button onclick="clearTextArea()" style="background: #e74c3c;">Clear</button>
529
+ </div>
530
+ </div>
531
+
532
+ <div class="control-group">
533
+ <label for="search-method">Search Method:</label>
534
+ <select id="search-method">
535
+ <option value="vector">Vector Search</option>
536
+ <option value="bm25">BM25 Search</option>
537
+ <option value="hybrid">Hybrid Search</option>
538
+ </select>
539
+ </div>
540
+
541
+ <div class="control-group">
542
+ <label for="result-limit">Results Limit:</label>
543
+ <input type="number" id="result-limit" min="1" max="50" value="10">
544
+ </div>
545
+
546
+ <div class="control-group">
547
+ <label for="book-filter">Filter by Books:</label>
548
+ <select id="book-filter" multiple style="height: 100px;">
549
+ <!-- Books will be populated dynamically -->
550
+ </select>
551
+ </div>
552
+
553
+ <button class="export-btn" onclick="exportToTEI()">Export as XML</button>
554
+ </div>
555
+
556
+ <div class="text-content" id="text-content">
557
+ <h2>De Imitatione Christi - Sample Text</h2>
558
+ <p>Qui sequitur me, non ambulat in tenebris, dicit Dominus. Haec sunt verba Christi, quibus admonemur, quatenus vitam ejus et mores imitemur, si volumus veraciter illuminari, et ab omni caecitate cordis liberari. Summum igitur studium nostrum sit, in vita Jesu Christi meditari.</p>
559
+
560
+ <p>Doctrina Christi omnes doctrinas sanctorum praecellit, et qui spiritum habet, inveniet ibi manna absconditum. Sed contingit, quod multi ex frequenti auditione Evangelii, parum desiderium sentiunt: quia spiritum Christi non habent. Qui autem vult plene et sapide verba Christi intelligere, oportet ut totam vitam suam illi studeat conformare.</p>
561
+
562
+ <p>Quid tibi prodest alta de Trinitate disputare, si cares humilitate, unde displiceas Trinitati? Vere alta verba non faciunt sanctum et justum; sed virtuosa vita efficit Deo carum. Opto magis sentire compunctionem, quam scire ejus definitionem.</p>
563
+
564
+ <p>Si scires totam Bibliam exterius, et omnium philosophorum dicta, quid totum prodesset sine caritate et gratia Dei? Vanitas vanitatum, et omnia vanitas, praeter amare Deum, et illi soli servire. Haec est summa sapientia: per contemptum mundi tendere ad regna caelestia.</p>
565
+
566
+ <p>Vanitas igitur est, honores perishables sectari, et ad alta loca ascendere. Vanitas est, carnis desideria sequi, et illud desiderare unde oporteat postea gravius puniri. Vanitas est, longam vitam optare, et de bona vita parum curare. Vanitas est, praesentem vitam tantum attendere, et quae futura sunt non prospicere.</p>
567
+ </div>
568
+ </div>
569
+ <div class="margin-area">
570
+ <h3>Marginalia</h3>
571
+ <div id="marginalia-list">
572
+ <p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>
573
+ </div>
574
+ </div>
575
+ </div>
576
+
577
+ <div class="popup-overlay" id="popup-overlay" onclick="closePopup()"></div>
578
+ <div class="popup" id="search-popup">
579
+ <div class="popup-header">
580
+ <div>
581
+ <h3>Similar Passages</h3>
582
+ <div id="query-text" style="margin-top: 5px; font-size: 0.9em; color: #666; font-style: italic; max-width: 400px; line-height: 1.3;"></div>
583
+ </div>
584
+ <div>
585
+ <small style="color: #666; margin-right: 15px;">Click results to add marginalia</small>
586
+ <button class="close-btn" onclick="closePopup()">Close</button>
587
+ </div>
588
+ </div>
589
+ <div id="search-results" class="search-results">
590
+ <!-- Results will be populated here -->
591
+ </div>
592
+ </div>
593
+
594
+ <script>
595
+ let selectedText = '';
596
+ let selectedElement = null;
597
+ let marginalia = [];
598
+ let books = [];
599
+ let searchInProgress = false; // Flag to prevent clearing during search
600
+
601
+ // Initialize the application
602
+ document.addEventListener('DOMContentLoaded', function() {
603
+ loadBooks();
604
+ initializeTextSelection();
605
+ testGradioConnection();
606
+ initializeTextArea();
607
+
608
+ // Global click handler to hide marginalia tooltips
609
+ document.addEventListener('click', function(e) {
610
+ // Don't interfere with textarea interactions
611
+ if (e.target.closest('#custom-text')) {
612
+ return;
613
+ }
614
+
615
+ // Don't hide if clicking on a marginalia marker or its tooltip
616
+ if (e.target.closest('.marginalia-marker') || e.target.closest('.marginalia-tooltip')) {
617
+ return;
618
+ }
619
+
620
+ // Hide all open tooltips
621
+ document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
622
+ tooltip.remove();
623
+ });
624
+ });
625
+ });
626
+
627
+ // Initialize textarea for proper keyboard handling
628
+ function initializeTextArea() {
629
+ const textArea = document.getElementById('custom-text');
630
+
631
+ // Ensure the textarea can receive focus and handle all keyboard events
632
+ textArea.setAttribute('tabindex', '0');
633
+
634
+ // Prevent event bubbling that might interfere with text editing
635
+ textArea.addEventListener('keydown', function(e) {
636
+ e.stopPropagation();
637
+ });
638
+
639
+ textArea.addEventListener('keyup', function(e) {
640
+ e.stopPropagation();
641
+ });
642
+
643
+ textArea.addEventListener('input', function(e) {
644
+ e.stopPropagation();
645
+ });
646
+
647
+ textArea.addEventListener('paste', function(e) {
648
+ e.stopPropagation();
649
+ });
650
+
651
+ textArea.addEventListener('cut', function(e) {
652
+ e.stopPropagation();
653
+ });
654
+
655
+ textArea.addEventListener('copy', function(e) {
656
+ e.stopPropagation();
657
+ });
658
+
659
+ // Ensure focus works properly
660
+ textArea.addEventListener('click', function(e) {
661
+ e.stopPropagation();
662
+ this.focus();
663
+ });
664
+ }
665
+
666
+ // Test connection to Gradio app
667
+ async function testGradioConnection() {
668
+ const statusIndicator = document.getElementById('status-indicator');
669
+ const statusText = document.getElementById('status-text');
670
+ const statusDiv = document.getElementById('connection-status');
671
+
672
+ try {
673
+ console.log('Testing connection to Gradio app...');
674
+
675
+ // First, check if the space is accessible
676
+ const spaceCheck = await fetch('https://medieval-data-latin-vulgate.hf.space/', {
677
+ method: 'HEAD'
678
+ });
679
+ console.log('Space accessibility check:', spaceCheck.status);
680
+
681
+ // Test the proper Gradio API pattern
682
+ console.log('Testing Gradio API with /gradio_api/call/predict endpoint...');
683
+
684
+ // Step 1: POST to get event_id
685
+ const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', {
686
+ method: 'POST',
687
+ headers: {
688
+ 'Content-Type': 'application/json',
689
+ },
690
+ body: JSON.stringify({
691
+ data: ["test", [], 1, "vector"]
692
+ })
693
+ });
694
+
695
+ console.log('POST response status:', postResponse.status);
696
+
697
+ if (!postResponse.ok) {
698
+ throw new Error(`API endpoint not available (${postResponse.status})`);
699
+ }
700
+
701
+ const postResult = await postResponse.json();
702
+ console.log('POST response:', postResult);
703
+
704
+ if (!postResult.event_id) {
705
+ throw new Error('Invalid API response - no event_id received');
706
+ }
707
+
708
+ console.log('Got event_id:', postResult.event_id);
709
+
710
+ // Step 2: Test GET endpoint (but don't wait for full response)
711
+ const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${postResult.event_id}`, {
712
+ method: 'GET',
713
+ });
714
+
715
+ console.log('GET response status:', getResponse.status);
716
+
717
+ if (getResponse.ok) {
718
+ statusIndicator.textContent = '✅';
719
+ statusText.textContent = 'Connected to search backend';
720
+ statusDiv.style.backgroundColor = '#d4edda';
721
+ statusDiv.style.color = '#155724';
722
+ statusDiv.style.border = '1px solid #c3e6cb';
723
+ console.log('API connection successful');
724
+
725
+ // Hide the API notice once connected
726
+ const apiNotice = document.getElementById('api-notice');
727
+ if (apiNotice) {
728
+ apiNotice.style.display = 'none';
729
+ }
730
+ } else {
731
+ throw new Error(`GET endpoint failed (${getResponse.status})`);
732
+ }
733
+
734
+ } catch (error) {
735
+ // Set up auto-retry every 30 seconds
736
+ statusIndicator.textContent = '🔄';
737
+ statusText.textContent = 'API not ready - will retry automatically...';
738
+ statusDiv.style.backgroundColor = '#fff3cd';
739
+ statusDiv.style.color = '#856404';
740
+ statusDiv.style.border = '1px solid #ffeaa7';
741
+
742
+ console.error('Gradio app connection test failed:', error);
743
+
744
+ setTimeout(() => {
745
+ console.log('Retrying API connection...');
746
+ testGradioConnection();
747
+ }, 30000); // Retry every 30 seconds
748
+ }
749
+ }
750
+
751
+ // Load available books from the Gradio app
752
+ async function loadBooks() {
753
+ try {
754
+ // Use the predefined book list since we can't easily get it from the Gradio API
755
+ const bookList = [
756
+ "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth",
757
+ "1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra",
758
+ "Nehemiah", "Tobit", "Judith", "Esther", "1 Maccabees", "2 Maccabees", "Job", "Psalms",
759
+ "Proverbs", "Ecclesiastes", "Song of Solomon", "Wisdom", "Sirach", "Isaiah", "Jeremiah",
760
+ "Lamentations", "Baruch", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah",
761
+ "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi",
762
+ "Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians",
763
+ "Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians",
764
+ "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter",
765
+ "1 John", "2 John", "3 John", "Jude", "Revelation"
766
+ ];
767
+
768
+ books = bookList;
769
+ const bookFilter = document.getElementById('book-filter');
770
+ books.forEach(book => {
771
+ const option = document.createElement('option');
772
+ option.value = book;
773
+ option.textContent = book;
774
+ bookFilter.appendChild(option);
775
+ });
776
+ } catch (error) {
777
+ console.error('Error loading books:', error);
778
+ }
779
+ }
780
+
781
+ // Initialize text selection functionality
782
+ function initializeTextSelection() {
783
+ const textContent = document.getElementById('text-content');
784
+
785
+ textContent.addEventListener('mouseup', function(e) {
786
+ const selection = window.getSelection();
787
+ if (selection.toString().trim().length > 0) {
788
+ selectedText = selection.toString().trim();
789
+ searchInProgress = true; // Set flag to prevent clearing
790
+
791
+ // Create a span around the selected text
792
+ if (selection.rangeCount > 0) {
793
+ const range = selection.getRangeAt(0);
794
+ const span = document.createElement('span');
795
+ span.className = 'selected-text';
796
+
797
+ try {
798
+ range.surroundContents(span);
799
+ selectedElement = span;
800
+
801
+ // Show search popup after a short delay
802
+ setTimeout(() => {
803
+ if (selectedText && searchInProgress) {
804
+ searchSimilarPassages(selectedText);
805
+ }
806
+ }, 100); // Reduced delay
807
+ } catch (error) {
808
+ // If surroundContents fails, clear selection
809
+ selection.removeAllRanges();
810
+ searchInProgress = false;
811
+ }
812
+ }
813
+ }
814
+ });
815
+
816
+ // Add click outside handler to clear temporary highlighting
817
+ document.addEventListener('click', function(e) {
818
+ // Don't interfere with textarea interactions
819
+ if (e.target.closest('#custom-text')) {
820
+ return;
821
+ }
822
+
823
+ // Don't clear if we're in the middle of a search
824
+ if (searchInProgress) {
825
+ return;
826
+ }
827
+
828
+ // Don't clear if clicking on popup or its contents
829
+ if (e.target.closest('#search-popup') || e.target.closest('.popup-overlay')) {
830
+ return;
831
+ }
832
+
833
+ // Don't clear if clicking on marginalia markers (they have their own tooltips)
834
+ if (e.target.closest('.marginalia-marker')) {
835
+ return;
836
+ }
837
+
838
+ // Don't clear if clicking on text content area (allow for text selection)
839
+ if (e.target.closest('#text-content') && window.getSelection().toString().trim().length > 0) {
840
+ return;
841
+ }
842
+
843
+ // Only clear if clicking outside the text content area
844
+ if (!e.target.closest('#text-content')) {
845
+ clearTemporarySelection();
846
+ }
847
+ });
848
+ }
849
+
850
+ // Clear temporary text selection (but keep marginalia markers)
851
+ function clearTemporarySelection() {
852
+ const tempSelected = document.querySelector('.selected-text');
853
+ if (tempSelected && !tempSelected.classList.contains('marginalia-marker')) {
854
+ const parent = tempSelected.parentNode;
855
+ while (tempSelected.firstChild) {
856
+ parent.insertBefore(tempSelected.firstChild, tempSelected);
857
+ }
858
+ parent.removeChild(tempSelected);
859
+ }
860
+
861
+ // Clear selection variables and reset flag
862
+ selectedText = '';
863
+ selectedElement = null;
864
+ searchInProgress = false;
865
+ window.getSelection().removeAllRanges();
866
+ }
867
+
868
+ // Parse HTML results from Gradio into JSON format
869
+ function parseGradioResults(htmlResult, query) {
870
+ try {
871
+ // Create a temporary DOM element to parse the HTML
872
+ const tempDiv = document.createElement('div');
873
+ tempDiv.innerHTML = htmlResult;
874
+
875
+ // Look for the table in the HTML
876
+ const table = tempDiv.querySelector('table');
877
+ if (!table) {
878
+ return [];
879
+ }
880
+
881
+ const rows = table.querySelectorAll('tbody tr');
882
+ const results = [];
883
+
884
+ rows.forEach(row => {
885
+ const cells = row.querySelectorAll('td');
886
+ if (cells.length >= 6) {
887
+ results.push({
888
+ reference: cells[0].textContent.trim(),
889
+ text: cells[1].innerHTML, // Keep HTML for highlighting
890
+ similarity: parseFloat(cells[2].textContent.trim()),
891
+ book: cells[3].textContent.trim(),
892
+ chapter: parseInt(cells[4].textContent.trim()),
893
+ verse: parseInt(cells[5].textContent.trim()),
894
+ raw_text: cells[1].textContent.trim() // Plain text version
895
+ });
896
+ }
897
+ });
898
+
899
+ return results;
900
+ } catch (error) {
901
+ console.error('Error parsing Gradio results:', error);
902
+ return [];
903
+ }
904
+ }
905
+
906
+ // Search for similar passages using Gradio API
907
+ async function searchSimilarPassages(query) {
908
+ const popup = document.getElementById('search-popup');
909
+ const overlay = document.getElementById('popup-overlay');
910
+ const results = document.getElementById('search-results');
911
+ const queryTextDiv = document.getElementById('query-text');
912
+
913
+ // Show popup with loading
914
+ results.innerHTML = '<div class="loading">Searching for similar passages...</div>';
915
+ popup.style.display = 'block';
916
+ overlay.style.display = 'block';
917
+
918
+ // Reset search flag once popup is shown
919
+ searchInProgress = false;
920
+
921
+ // Display the original query text
922
+ queryTextDiv.textContent = `Query: "${query}"`;
923
+
924
+ try {
925
+ const searchMethod = document.getElementById('search-method').value;
926
+ const resultLimit = parseInt(document.getElementById('result-limit').value);
927
+ const selectedBooks = Array.from(document.getElementById('book-filter').selectedOptions)
928
+ .map(option => option.value);
929
+
930
+ console.log(`Searching with query: "${query}"`);
931
+
932
+ // Step 1: POST to /gradio_api/call/predict to get event_id
933
+ const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', {
934
+ method: 'POST',
935
+ headers: {
936
+ 'Content-Type': 'application/json',
937
+ },
938
+ body: JSON.stringify({
939
+ data: [query, selectedBooks, resultLimit, searchMethod]
940
+ })
941
+ });
942
+
943
+ if (!postResponse.ok) {
944
+ throw new Error(`POST request failed: ${postResponse.status} ${postResponse.statusText}`);
945
+ }
946
+
947
+ const postResult = await postResponse.json();
948
+ console.log('POST response:', postResult);
949
+
950
+ if (!postResult.event_id) {
951
+ throw new Error('No event_id received from server');
952
+ }
953
+
954
+ const eventId = postResult.event_id;
955
+ console.log('Got event_id:', eventId);
956
+
957
+ // Step 2: GET to /gradio_api/call/predict/{event_id} to stream results
958
+ const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${eventId}`, {
959
+ method: 'GET',
960
+ });
961
+
962
+ if (!getResponse.ok) {
963
+ throw new Error(`GET request failed: ${getResponse.status} ${getResponse.statusText}`);
964
+ }
965
+
966
+ // Parse Server-Sent Events stream
967
+ const reader = getResponse.body.getReader();
968
+ const decoder = new TextDecoder();
969
+ let buffer = '';
970
+ let finalResult = null;
971
+
972
+ while (true) {
973
+ const { done, value } = await reader.read();
974
+
975
+ if (done) break;
976
+
977
+ buffer += decoder.decode(value, { stream: true });
978
+ const lines = buffer.split('\n');
979
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
980
+
981
+ for (const line of lines) {
982
+ if (line.startsWith('data: ')) {
983
+ try {
984
+ const data = JSON.parse(line.slice(6));
985
+ console.log('Received data:', data);
986
+ if (Array.isArray(data) && data.length > 0) {
987
+ finalResult = data[0]; // HTML result should be in first element
988
+ }
989
+ } catch (error) {
990
+ console.log('Error parsing data line:', error);
991
+ }
992
+ } else if (line.startsWith('event: ')) {
993
+ const eventType = line.slice(8);
994
+ console.log('Event type:', eventType);
995
+
996
+ if (eventType === 'complete' && finalResult) {
997
+ // We have the final result
998
+ const searchResults = parseGradioResults(finalResult, query);
999
+ displaySearchResults(searchResults, query);
1000
+ return;
1001
+ } else if (eventType === 'error') {
1002
+ throw new Error('Server returned error event');
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ if (finalResult) {
1009
+ const searchResults = parseGradioResults(finalResult, query);
1010
+ displaySearchResults(searchResults, query);
1011
+ } else {
1012
+ throw new Error('No results received from server');
1013
+ }
1014
+
1015
+ } catch (error) {
1016
+ console.error('Error searching:', error);
1017
+ results.innerHTML = `
1018
+ <div class="error">
1019
+ <strong>Search Error:</strong> ${error.message}<br><br>
1020
+ <strong>Troubleshooting:</strong><br>
1021
+ 1. Check if your <a href="https://huggingface.co/spaces/medieval-data/latin-vulgate" target="_blank">HuggingFace Space</a> is running<br>
1022
+ 2. Try a "Factory restart" in Space settings<br>
1023
+ 3. Verify the Space has <code>api_name="predict"</code> in the Gradio interface<br>
1024
+ 4. Wait 2-3 minutes after restart for API to become available
1025
+ </div>
1026
+ `;
1027
+ }
1028
+ }
1029
+
1030
+ // Function removed - using direct Gradio API calls
1031
+
1032
+
1033
+
1034
+ // Display search results
1035
+ function displaySearchResults(results, originalQuery) {
1036
+ const resultsContainer = document.getElementById('search-results');
1037
+ const queryTextDiv = document.getElementById('query-text');
1038
+
1039
+ // Make sure the query text is displayed
1040
+ queryTextDiv.innerHTML = `<strong>Query:</strong> "${originalQuery}"`;
1041
+
1042
+ if (results.length === 0) {
1043
+ resultsContainer.innerHTML = '<div class="loading">No similar passages found.</div>';
1044
+ return;
1045
+ }
1046
+
1047
+ let html = '';
1048
+ results.forEach((result, index) => {
1049
+ const resultId = `result-${Date.now()}-${index}`;
1050
+ html += `
1051
+ <div class="result-item" data-result-id="${resultId}" data-original-query="${originalQuery}">
1052
+ <div class="result-reference">${result.reference}</div>
1053
+ <div class="result-text">${result.text}</div>
1054
+ <div class="result-similarity">Similarity: ${result.similarity}</div>
1055
+ </div>
1056
+ `;
1057
+
1058
+ // Store result data globally for easy access
1059
+ window.searchResults = window.searchResults || {};
1060
+ window.searchResults[resultId] = {
1061
+ reference: result.reference,
1062
+ text: result.text,
1063
+ similarity: result.similarity,
1064
+ raw_text: result.raw_text || result.text
1065
+ };
1066
+ });
1067
+
1068
+ resultsContainer.innerHTML = html;
1069
+
1070
+ // Add click event listeners to result items
1071
+ resultsContainer.querySelectorAll('.result-item').forEach(item => {
1072
+ item.addEventListener('click', function() {
1073
+ const resultId = this.getAttribute('data-result-id');
1074
+ const originalQuery = this.getAttribute('data-original-query');
1075
+ const resultData = window.searchResults[resultId];
1076
+
1077
+ if (resultData) {
1078
+ addMarginalia(originalQuery, resultData);
1079
+ }
1080
+ });
1081
+ });
1082
+ }
1083
+
1084
+ // Add marginalia annotation
1085
+ function addMarginalia(originalText, result) {
1086
+ const marginaliaId = Date.now().toString();
1087
+
1088
+ // Add to marginalia array
1089
+ marginalia.push({
1090
+ id: marginaliaId,
1091
+ originalText: originalText,
1092
+ reference: result.reference,
1093
+ text: result.raw_text,
1094
+ similarity: result.similarity
1095
+ });
1096
+
1097
+ // Update the selected text element
1098
+ if (selectedElement) {
1099
+ selectedElement.className = 'marginalia-marker';
1100
+ selectedElement.setAttribute('data-marginalia-id', marginaliaId);
1101
+
1102
+ // Create or update marginalia indicator
1103
+ updateMarginaliaIndicator(selectedElement);
1104
+
1105
+ // Add hover events for tooltip
1106
+ setupMarginaliaHover(selectedElement);
1107
+ }
1108
+
1109
+ // Add marginalia to the margin area
1110
+ addMarginaliaToMargin(marginaliaId);
1111
+
1112
+ // Auto-close popup after adding marginalia
1113
+ closePopup();
1114
+
1115
+ // Clear selection variables
1116
+ selectedText = '';
1117
+ selectedElement = null;
1118
+ window.getSelection().removeAllRanges();
1119
+ }
1120
+
1121
+ // Add marginalia to the margin area
1122
+ function addMarginaliaToMargin(marginaliaId) {
1123
+ const marginList = document.getElementById('marginalia-list');
1124
+ const marginaliaItem = marginalia.find(item => item.id === marginaliaId);
1125
+
1126
+ if (!marginaliaItem) return;
1127
+
1128
+ // Remove placeholder text if present
1129
+ const placeholder = marginList.querySelector('p');
1130
+ if (placeholder) {
1131
+ placeholder.remove();
1132
+ }
1133
+
1134
+ // Create margin note
1135
+ const marginNote = document.createElement('div');
1136
+ marginNote.className = 'margin-note';
1137
+ marginNote.setAttribute('data-marginalia-id', marginaliaId);
1138
+
1139
+ // Truncate text for margin display
1140
+ const truncatedText = truncateText(marginaliaItem.text, 120);
1141
+
1142
+ marginNote.innerHTML = `
1143
+ <div class="margin-note-reference">${marginaliaItem.reference}</div>
1144
+ <div class="margin-note-text" title="${marginaliaItem.text}">${truncatedText}</div>
1145
+ <div class="margin-note-similarity">Similarity: ${marginaliaItem.similarity}</div>
1146
+ <button class="margin-note-remove" onclick="removeMarginalia('${marginaliaId}')" title="Remove marginalia">×</button>
1147
+ `;
1148
+
1149
+ // Add hover effects to highlight corresponding text
1150
+ marginNote.addEventListener('mouseenter', function() {
1151
+ highlightCorrespondingText(marginaliaId, true);
1152
+ });
1153
+
1154
+ marginNote.addEventListener('mouseleave', function() {
1155
+ highlightCorrespondingText(marginaliaId, false);
1156
+ });
1157
+
1158
+ // Add click to scroll to text
1159
+ marginNote.addEventListener('click', function() {
1160
+ scrollToText(marginaliaId);
1161
+ });
1162
+
1163
+ marginList.appendChild(marginNote);
1164
+ }
1165
+
1166
+ // Highlight corresponding text when hovering over margin note
1167
+ function highlightCorrespondingText(marginaliaId, highlight) {
1168
+ const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`);
1169
+ const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`);
1170
+
1171
+ if (textMarker) {
1172
+ if (highlight) {
1173
+ textMarker.style.backgroundColor = '#ffffcc';
1174
+ textMarker.style.outline = '2px solid #3498db';
1175
+ } else {
1176
+ textMarker.style.backgroundColor = '#e8f4fd';
1177
+ textMarker.style.outline = '';
1178
+ }
1179
+ }
1180
+
1181
+ if (marginNote) {
1182
+ if (highlight) {
1183
+ marginNote.classList.add('highlighted');
1184
+ } else {
1185
+ marginNote.classList.remove('highlighted');
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ // Scroll to corresponding text when clicking margin note
1191
+ function scrollToText(marginaliaId) {
1192
+ const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`);
1193
+ if (textMarker) {
1194
+ textMarker.scrollIntoView({
1195
+ behavior: 'smooth',
1196
+ block: 'center'
1197
+ });
1198
+
1199
+ // Temporarily highlight the text
1200
+ highlightCorrespondingText(marginaliaId, true);
1201
+ setTimeout(() => {
1202
+ highlightCorrespondingText(marginaliaId, false);
1203
+ }, 2000);
1204
+ }
1205
+ }
1206
+
1207
+ // Update marginalia indicator (shows count if multiple)
1208
+ function updateMarginaliaIndicator(element) {
1209
+ // Remove existing indicator
1210
+ const existingIndicator = element.querySelector('.marginalia-indicator');
1211
+ if (existingIndicator) {
1212
+ existingIndicator.remove();
1213
+ }
1214
+
1215
+ // Get all marginalia for this element
1216
+ const elementMarginalia = marginalia.filter(item =>
1217
+ element.getAttribute('data-marginalia-id') === item.id ||
1218
+ element.getAttribute('data-marginalia-ids')?.split(',').includes(item.id)
1219
+ );
1220
+
1221
+ // Handle multiple marginalia on same text
1222
+ const allIds = element.getAttribute('data-marginalia-id') ?
1223
+ [element.getAttribute('data-marginalia-id')] : [];
1224
+
1225
+ if (elementMarginalia.length > 0) {
1226
+ const indicator = document.createElement('div');
1227
+ indicator.className = elementMarginalia.length > 1 ?
1228
+ 'marginalia-indicator multiple' : 'marginalia-indicator';
1229
+ indicator.textContent = elementMarginalia.length > 1 ?
1230
+ elementMarginalia.length.toString() : '•';
1231
+ element.appendChild(indicator);
1232
+
1233
+ // Update data attribute for multiple marginalia
1234
+ if (elementMarginalia.length > 1) {
1235
+ element.setAttribute('data-marginalia-ids',
1236
+ elementMarginalia.map(m => m.id).join(','));
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ // Setup hover events for marginalia tooltips
1242
+ function setupMarginaliaHover(element) {
1243
+ // Remove any existing listeners to avoid duplicates
1244
+ element.removeEventListener('mouseenter', showMarginaliaTooltip);
1245
+ element.removeEventListener('mouseleave', hideMarginaliaTooltip);
1246
+ element.removeEventListener('click', showMarginaliaTooltip);
1247
+
1248
+ // Add both hover and click events for better usability
1249
+ element.addEventListener('mouseenter', function(e) {
1250
+ showMarginaliaTooltip(e);
1251
+ // Also highlight corresponding margin notes
1252
+ const marginaliaId = element.getAttribute('data-marginalia-id');
1253
+ if (marginaliaId) {
1254
+ highlightCorrespondingText(marginaliaId, true);
1255
+ }
1256
+ });
1257
+
1258
+ element.addEventListener('mouseleave', function(e) {
1259
+ hideMarginaliaTooltip(e);
1260
+ // Remove highlight from margin notes
1261
+ const marginaliaId = element.getAttribute('data-marginalia-id');
1262
+ if (marginaliaId) {
1263
+ highlightCorrespondingText(marginaliaId, false);
1264
+ }
1265
+ });
1266
+
1267
+ element.addEventListener('click', function(e) {
1268
+ e.stopPropagation();
1269
+ showMarginaliaTooltip(e);
1270
+ });
1271
+ }
1272
+
1273
+ // Show marginalia tooltip on hover
1274
+ function showMarginaliaTooltip(event) {
1275
+ const element = event.target.closest('.marginalia-marker');
1276
+ if (!element) return;
1277
+
1278
+ const marginaliaId = element.getAttribute('data-marginalia-id');
1279
+ const marginaliaIds = element.getAttribute('data-marginalia-ids');
1280
+
1281
+ console.log('Showing tooltip for element:', element, 'ID:', marginaliaId, 'IDs:', marginaliaIds);
1282
+
1283
+ let relevantMarginalia = [];
1284
+
1285
+ if (marginaliaIds) {
1286
+ // Multiple marginalia
1287
+ const ids = marginaliaIds.split(',');
1288
+ relevantMarginalia = marginalia.filter(item => ids.includes(item.id));
1289
+ } else if (marginaliaId) {
1290
+ // Single marginalia
1291
+ relevantMarginalia = marginalia.filter(item => item.id === marginaliaId);
1292
+ }
1293
+
1294
+ console.log('Relevant marginalia:', relevantMarginalia);
1295
+
1296
+ if (relevantMarginalia.length === 0) return;
1297
+
1298
+ // Remove any existing tooltip
1299
+ document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
1300
+ tooltip.remove();
1301
+ });
1302
+
1303
+ // Create tooltip
1304
+ const tooltip = document.createElement('div');
1305
+ tooltip.className = 'marginalia-tooltip show';
1306
+
1307
+ let tooltipHTML = '';
1308
+ relevantMarginalia.forEach(item => {
1309
+ tooltipHTML += `
1310
+ <div class="marginalia-tooltip-item">
1311
+ <div class="marginalia-tooltip-reference">${item.reference}</div>
1312
+ <div class="marginalia-tooltip-text">${item.text}</div>
1313
+ <div class="marginalia-tooltip-similarity">Similarity: ${item.similarity}</div>
1314
+ </div>
1315
+ `;
1316
+ });
1317
+
1318
+ tooltip.innerHTML = tooltipHTML;
1319
+
1320
+ // Position tooltip relative to document body for better positioning
1321
+ document.body.appendChild(tooltip);
1322
+
1323
+ // Position tooltip relative to the element
1324
+ const rect = element.getBoundingClientRect();
1325
+ const tooltipRect = tooltip.getBoundingClientRect();
1326
+
1327
+ let left = rect.left;
1328
+ let top = rect.bottom + window.scrollY + 5;
1329
+
1330
+ // Adjust if tooltip would go off screen
1331
+ if (left + tooltipRect.width > window.innerWidth) {
1332
+ left = window.innerWidth - tooltipRect.width - 10;
1333
+ }
1334
+
1335
+ if (top + tooltipRect.height > window.innerHeight + window.scrollY) {
1336
+ top = rect.top + window.scrollY - tooltipRect.height - 5;
1337
+ }
1338
+
1339
+ tooltip.style.position = 'absolute';
1340
+ tooltip.style.left = left + 'px';
1341
+ tooltip.style.top = top + 'px';
1342
+ tooltip.style.zIndex = '9999';
1343
+
1344
+ console.log('Tooltip positioned at:', left, top);
1345
+
1346
+ // Store reference to the tooltip on the element for easy removal
1347
+ element._tooltip = tooltip;
1348
+ }
1349
+
1350
+ // Hide marginalia tooltip
1351
+ function hideMarginaliaTooltip(event) {
1352
+ const element = event.target.closest('.marginalia-marker');
1353
+ if (!element) return;
1354
+
1355
+ // Remove the tooltip after a short delay to allow clicking on it
1356
+ setTimeout(() => {
1357
+ if (element._tooltip) {
1358
+ element._tooltip.remove();
1359
+ element._tooltip = null;
1360
+ }
1361
+ }, 100);
1362
+ }
1363
+
1364
+
1365
+
1366
+ // Remove marginalia
1367
+ function removeMarginalia(marginaliaId) {
1368
+ console.log('Removing marginalia:', marginaliaId);
1369
+
1370
+ // Remove from array
1371
+ marginalia = marginalia.filter(item => item.id !== marginaliaId);
1372
+
1373
+ // Remove from margin area
1374
+ const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`);
1375
+ if (marginNote) {
1376
+ marginNote.remove();
1377
+ }
1378
+
1379
+ // Add placeholder text if no marginalia left
1380
+ const marginList = document.getElementById('marginalia-list');
1381
+ if (marginalia.length === 0) {
1382
+ marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>';
1383
+ }
1384
+
1385
+ // Find the marker element
1386
+ const marker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`) ||
1387
+ document.querySelector(`[data-marginalia-ids*="${marginaliaId}"]`);
1388
+
1389
+ if (marker) {
1390
+ // Handle multiple marginalia on same element
1391
+ const marginaliaIds = marker.getAttribute('data-marginalia-ids');
1392
+
1393
+ if (marginaliaIds) {
1394
+ // Multiple marginalia - remove this one and update
1395
+ const remainingIds = marginaliaIds.split(',').filter(id => id !== marginaliaId);
1396
+
1397
+ if (remainingIds.length > 1) {
1398
+ // Still multiple marginalia
1399
+ marker.setAttribute('data-marginalia-ids', remainingIds.join(','));
1400
+ updateMarginaliaIndicator(marker);
1401
+ } else if (remainingIds.length === 1) {
1402
+ // Only one left - convert back to single
1403
+ marker.removeAttribute('data-marginalia-ids');
1404
+ marker.setAttribute('data-marginalia-id', remainingIds[0]);
1405
+ updateMarginaliaIndicator(marker);
1406
+ } else {
1407
+ // None left - remove marker entirely
1408
+ const parent = marker.parentNode;
1409
+ while (marker.firstChild) {
1410
+ parent.insertBefore(marker.firstChild, marker);
1411
+ }
1412
+ parent.removeChild(marker);
1413
+ }
1414
+ } else {
1415
+ // Single marginalia - remove marker entirely
1416
+ const parent = marker.parentNode;
1417
+ while (marker.firstChild) {
1418
+ parent.insertBefore(marker.firstChild, marker);
1419
+ }
1420
+ parent.removeChild(marker);
1421
+ }
1422
+
1423
+ // Clean up any existing tooltip
1424
+ if (marker._tooltip) {
1425
+ marker._tooltip.remove();
1426
+ marker._tooltip = null;
1427
+ }
1428
+ }
1429
+
1430
+ // Hide any other open tooltips
1431
+ document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
1432
+ tooltip.remove();
1433
+ });
1434
+ }
1435
+
1436
+ // Close popup
1437
+ function closePopup() {
1438
+ document.getElementById('search-popup').style.display = 'none';
1439
+ document.getElementById('popup-overlay').style.display = 'none';
1440
+ // Clear any temporary text selection when popup closes
1441
+ clearTemporarySelection();
1442
+ }
1443
+
1444
+ // Clear text selection
1445
+ function clearSelection() {
1446
+ clearTemporarySelection();
1447
+ }
1448
+
1449
+ // Export to TEI XML
1450
+ function exportToTEI() {
1451
+ const textContent = document.getElementById('text-content').cloneNode(true);
1452
+
1453
+ // Remove control elements
1454
+ const controls = textContent.querySelector('.controls');
1455
+ if (controls) controls.remove();
1456
+
1457
+ // Get the clean HTML content
1458
+ const cleanHtml = textContent.innerHTML;
1459
+
1460
+ // Generate TEI XML
1461
+ const teiXml = generateTEIXML(cleanHtml);
1462
+
1463
+ // Create download
1464
+ const blob = new Blob([teiXml], { type: 'application/xml' });
1465
+ const url = URL.createObjectURL(blob);
1466
+ const a = document.createElement('a');
1467
+ a.href = url;
1468
+ a.download = 'vulgate-marginalia.xml';
1469
+ document.body.appendChild(a);
1470
+ a.click();
1471
+ document.body.removeChild(a);
1472
+ URL.revokeObjectURL(url);
1473
+ }
1474
+
1475
+ // Generate TEI XML format
1476
+ function generateTEIXML(htmlContent) {
1477
+ const timestamp = new Date().toISOString();
1478
+
1479
+ console.log('Original HTML content:', htmlContent);
1480
+
1481
+ // Create a temporary div to work with the HTML
1482
+ const tempDiv = document.createElement('div');
1483
+ tempDiv.innerHTML = htmlContent;
1484
+
1485
+ // Remove control elements
1486
+ const controls = tempDiv.querySelector('.controls');
1487
+ if (controls) controls.remove();
1488
+
1489
+ console.log('HTML after removing controls:', tempDiv.innerHTML);
1490
+
1491
+ // Clean up HTML and convert to proper TEI structure
1492
+ const cleanText = cleanHtmlToTei(tempDiv.innerHTML);
1493
+
1494
+ console.log('Cleaned TEI text:', cleanText);
1495
+ console.log('Current marginalia array:', marginalia);
1496
+
1497
+ let teiHeader = `<?xml version="1.0" encoding="UTF-8"?>
1498
+ <TEI xmlns="http://www.tei-c.org/ns/1.0">
1499
+ <teiHeader>
1500
+ <fileDesc>
1501
+ <titleStmt>
1502
+ <title>Latin Vulgate Text with Marginalia</title>
1503
+ <author>Generated by Vulgate Text Editor</author>
1504
+ </titleStmt>
1505
+ <publicationStmt>
1506
+ <p>Generated on ${timestamp}</p>
1507
+ </publicationStmt>
1508
+ <sourceDesc>
1509
+ <p>Latin Vulgate text with semantic similarity annotations</p>
1510
+ </sourceDesc>
1511
+ </fileDesc>
1512
+ </teiHeader>
1513
+ <text>
1514
+ <body>
1515
+ ${cleanText}`;
1516
+
1517
+ // Add marginalia annotations in proper TEI format
1518
+ if (marginalia.length > 0) {
1519
+ teiHeader += '\n <div type="apparatus">\n';
1520
+ marginalia.forEach(item => {
1521
+ // Parse the reference to get book, chapter, verse
1522
+ const refParts = parseReference(item.reference);
1523
+
1524
+ teiHeader += ` <note xml:id="${item.id}" type="parallel" resp="#semantic-analysis">
1525
+ <bibl>
1526
+ <title type="biblical-book">${refParts.book}</title>
1527
+ <biblScope unit="chapter">${refParts.chapter}</biblScope>
1528
+ <biblScope unit="verse">${refParts.verse}</biblScope>
1529
+ </bibl>
1530
+ <quote xml:lang="la">${escapeXml(item.text)}</quote>
1531
+ <measure type="similarity" quantity="${item.similarity}"/>
1532
+ </note>\n`;
1533
+ });
1534
+ teiHeader += ' </div>\n';
1535
+ }
1536
+
1537
+ const teiFooter = ` </body>
1538
+ </text>
1539
+ </TEI>`;
1540
+
1541
+ const finalXml = teiHeader + teiFooter;
1542
+ console.log('Final XML:', finalXml);
1543
+
1544
+ return finalXml;
1545
+ }
1546
+
1547
+ // Clean HTML and convert to TEI structure
1548
+ function cleanHtmlToTei(htmlContent) {
1549
+ let teiContent = htmlContent;
1550
+
1551
+ console.log('Starting cleanHtmlToTei with:', teiContent);
1552
+
1553
+ // Convert headings first
1554
+ teiContent = teiContent.replace(/<h2[^>]*>(.*?)<\/h2>/g, ' <head>$1</head>');
1555
+
1556
+ // Process marginalia markers - be more comprehensive with the regex patterns
1557
+ // Handle markers with indicators (most comprehensive pattern)
1558
+ let replacements = 0;
1559
+ teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div><\/span>/gs,
1560
+ (match, id, text) => {
1561
+ console.log(`Replacing marginalia marker ${id} with text: "${text}"`);
1562
+ replacements++;
1563
+ return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
1564
+ });
1565
+
1566
+ // Handle simpler marginalia markers without indicators
1567
+ teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<\/span>/gs,
1568
+ (match, id, text) => {
1569
+ console.log(`Replacing simple marginalia marker ${id} with text: "${text}"`);
1570
+ replacements++;
1571
+ return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
1572
+ });
1573
+
1574
+ // Alternative order - data-marginalia-id might come before class
1575
+ teiContent = teiContent.replace(/<span[^>]*data-marginalia-id="([^"]*)"[^>]*class="marginalia-marker"[^>]*>(.*?)<\/span>/gs,
1576
+ (match, id, text) => {
1577
+ console.log(`Replacing alt-order marginalia marker ${id} with text: "${text}"`);
1578
+ replacements++;
1579
+ return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
1580
+ });
1581
+
1582
+ console.log(`Made ${replacements} marginalia replacements`);
1583
+
1584
+ // Remove any remaining HTML artifacts
1585
+ teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*>/g, '<seg>');
1586
+ teiContent = teiContent.replace(/<\/span>/g, '</seg>');
1587
+ teiContent = teiContent.replace(/<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div>/gs, '');
1588
+
1589
+ // Remove any style attributes and other HTML artifacts
1590
+ teiContent = teiContent.replace(/style="[^"]*"/g, '');
1591
+ teiContent = teiContent.replace(/class="[^"]*"/g, '');
1592
+ teiContent = teiContent.replace(/data-[^=]*="[^"]*"/g, '');
1593
+
1594
+ // Clean up paragraphs - add proper indentation while preserving content
1595
+ teiContent = teiContent.replace(/<p>/g, ' <p>');
1596
+ teiContent = teiContent.replace(/<\/p>/g, '</p>');
1597
+
1598
+ // Remove empty paragraphs
1599
+ teiContent = teiContent.replace(/\s*<p>\s*<\/p>\s*/g, '');
1600
+
1601
+ // Clean up extra whitespace but preserve text flow
1602
+ teiContent = teiContent.replace(/\s+>/g, '>');
1603
+ teiContent = teiContent.replace(/>\s+</g, '><');
1604
+
1605
+ // Ensure proper line breaks between elements
1606
+ teiContent = teiContent.replace(/(<\/p>)(?=\s*<)/g, '$1\n');
1607
+ teiContent = teiContent.replace(/(<\/head>)(?=\s*<)/g, '$1\n');
1608
+
1609
+ // Final cleanup - normalize whitespace
1610
+ teiContent = teiContent.trim();
1611
+
1612
+ console.log('Final cleaned TEI content:', teiContent);
1613
+
1614
+ return teiContent;
1615
+ }
1616
+
1617
+ // Parse biblical reference into components
1618
+ function parseReference(reference) {
1619
+ // Handle references like "Jo 8:12", "1Cor 13:4", "Mt 5:3-4"
1620
+ const match = reference.match(/^(\d?\s*\w+)\s+(\d+):(\d+)(?:-\d+)?$/);
1621
+
1622
+ if (match) {
1623
+ return {
1624
+ book: match[1].trim(),
1625
+ chapter: match[2],
1626
+ verse: match[3]
1627
+ };
1628
+ }
1629
+
1630
+ // Fallback for non-standard formats
1631
+ return {
1632
+ book: reference,
1633
+ chapter: "1",
1634
+ verse: "1"
1635
+ };
1636
+ }
1637
+
1638
+ // Escape XML special characters
1639
+ function escapeXml(text) {
1640
+ return text
1641
+ .replace(/&/g, '&amp;')
1642
+ .replace(/</g, '&lt;')
1643
+ .replace(/>/g, '&gt;')
1644
+ .replace(/"/g, '&quot;')
1645
+ .replace(/'/g, '&#39;');
1646
+ }
1647
+
1648
+ // Utility function to truncate text
1649
+ function truncateText(text, maxLength) {
1650
+ if (text.length <= maxLength) return text;
1651
+
1652
+ // Find the last space before the max length to avoid cutting words
1653
+ const truncated = text.substring(0, maxLength);
1654
+ const lastSpace = truncated.lastIndexOf(' ');
1655
+
1656
+ if (lastSpace > maxLength * 0.8) {
1657
+ return truncated.substring(0, lastSpace) + '...';
1658
+ }
1659
+
1660
+ return truncated + '...';
1661
+ }
1662
+
1663
+ // Function to load custom text into the editor
1664
+ function loadCustomText() {
1665
+ const customTextArea = document.getElementById('custom-text');
1666
+ const textContent = document.getElementById('text-content');
1667
+
1668
+ if (customTextArea.value.trim()) {
1669
+ const inputText = customTextArea.value.trim();
1670
+
1671
+ // Clear any existing marginalia when loading new text
1672
+ clearAllMarginalia();
1673
+
1674
+ // Format the text properly
1675
+ let formattedText = '';
1676
+
1677
+ // Check if input already contains HTML
1678
+ if (inputText.includes('<') && inputText.includes('>')) {
1679
+ // Input appears to be HTML - use as is but ensure proper structure
1680
+ formattedText = inputText;
1681
+ } else {
1682
+ // Plain text - convert to proper HTML paragraphs
1683
+ // Split by double line breaks for paragraphs
1684
+ const paragraphs = inputText.split(/\n\s*\n/);
1685
+
1686
+ // Add a default title if none present
1687
+ if (!inputText.toLowerCase().includes('<h') && paragraphs.length > 0) {
1688
+ formattedText = '<h2>Custom Text</h2>\n';
1689
+ }
1690
+
1691
+ // Convert each paragraph
1692
+ paragraphs.forEach(para => {
1693
+ const cleanPara = para.replace(/\n/g, ' ').trim();
1694
+ if (cleanPara) {
1695
+ formattedText += `<p>${cleanPara}</p>\n`;
1696
+ }
1697
+ });
1698
+ }
1699
+
1700
+ textContent.innerHTML = formattedText;
1701
+
1702
+ // Clear the textarea
1703
+ customTextArea.value = '';
1704
+
1705
+ // Re-initialize text selection
1706
+ initializeTextSelection();
1707
+
1708
+ // Show success message
1709
+ showStatusMessage('Custom text loaded successfully!', 'success');
1710
+
1711
+ } else {
1712
+ showStatusMessage('Please paste your text into the textarea first.', 'error');
1713
+ }
1714
+ }
1715
+
1716
+ // Function to clear all marginalia
1717
+ function clearAllMarginalia() {
1718
+ marginalia = [];
1719
+ const marginList = document.getElementById('marginalia-list');
1720
+ marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>';
1721
+
1722
+ // Remove all marginalia markers from text
1723
+ document.querySelectorAll('.marginalia-marker').forEach(marker => {
1724
+ const parent = marker.parentNode;
1725
+ while (marker.firstChild) {
1726
+ parent.insertBefore(marker.firstChild, marker);
1727
+ }
1728
+ parent.removeChild(marker);
1729
+ });
1730
+
1731
+ // Clear any open tooltips
1732
+ document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
1733
+ tooltip.remove();
1734
+ });
1735
+ }
1736
+
1737
+ // Function to show status messages
1738
+ function showStatusMessage(message, type = 'info') {
1739
+ const existingMessage = document.getElementById('status-message');
1740
+ if (existingMessage) {
1741
+ existingMessage.remove();
1742
+ }
1743
+
1744
+ const messageDiv = document.createElement('div');
1745
+ messageDiv.id = 'status-message';
1746
+ messageDiv.style.cssText = `
1747
+ position: fixed;
1748
+ top: 20px;
1749
+ right: 20px;
1750
+ padding: 12px 20px;
1751
+ border-radius: 4px;
1752
+ color: white;
1753
+ font-weight: bold;
1754
+ z-index: 10000;
1755
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
1756
+ ${type === 'success' ? 'background: #27ae60;' : ''}
1757
+ ${type === 'error' ? 'background: #e74c3c;' : ''}
1758
+ ${type === 'info' ? 'background: #3498db;' : ''}
1759
+ `;
1760
+ messageDiv.textContent = message;
1761
+
1762
+ document.body.appendChild(messageDiv);
1763
+
1764
+ // Auto-remove after 3 seconds
1765
+ setTimeout(() => {
1766
+ if (messageDiv.parentNode) {
1767
+ messageDiv.remove();
1768
+ }
1769
+ }, 3000);
1770
+ }
1771
+
1772
+ // Function to reset text to sample
1773
+ function resetToSample() {
1774
+ const textContent = document.getElementById('text-content');
1775
+ textContent.innerHTML = `
1776
+ <h2>De Imitatione Christi - Sample Text</h2>
1777
+ <p>Qui sequitur me, non ambulat in tenebris, dicit Dominus. Haec sunt verba Christi, quibus admonemur, quatenus vitam ejus et mores imitemur, si volumus veraciter illuminari, et ab omni caecitate cordis liberari. Summum igitur studium nostrum sit, in vita Jesu Christi meditari.</p>
1778
+
1779
+ <p>Doctrina Christi omnes doctrinas sanctorum praecellit, et qui spiritum habet, inveniet ibi manna absconditum. Sed contingit, quod multi ex frequenti auditione Evangelii, parum desiderium sentiunt: quia spiritum Christi non habent. Qui autem vult plene et sapide verba Christi intelligere, oportet ut totam vitam suam illi studeat conformare.</p>
1780
+
1781
+ <p>Quid tibi prodest alta de Trinitate disputare, si cares humilitate, unde displiceas Trinitati? Vere alta verba non faciunt sanctum et justum; sed virtuosa vita efficit Deo carum. Opto magis sentire compunctionem, quam scire ejus definitionem.</p>
1782
+
1783
+ <p>Si scires totam Bibliam exterius, et omnium philosophorum dicta, quid totum prodesset sine caritate et gratia Dei? Vanitas vanitatum, et omnia vanitas, praeter amare Deum, et illi soli servire. Haec est summa sapientia: per contemptum mundi tendere ad regna caelestia.</p>
1784
+
1785
+ <p>Vanitas igitur est, honores perishables sectari, et ad alta loca ascendere. Vanitas est, carnis desideria sequi, et illud desiderare unde oporteat postea gravius puniri. Vanitas est, longam vitam optare, et de bona vita parum curare. Vanitas est, praesentem vitam tantum attendere, et quae futura sunt non prospicere.</p>
1786
+ `;
1787
+ // Clear any existing marginalia when resetting
1788
+ clearAllMarginalia();
1789
+
1790
+ // Re-initialize text selection
1791
+ initializeTextSelection();
1792
+
1793
+ // Show success message
1794
+ showStatusMessage('Reset to sample text successfully!', 'success');
1795
+ }
1796
+
1797
+ // Function to clear the custom text area
1798
+ function clearTextArea() {
1799
+ const customTextArea = document.getElementById('custom-text');
1800
+ customTextArea.value = '';
1801
+ showStatusMessage('Custom text area cleared.', 'info');
1802
+ }
1803
+ </script>
1804
+ </body>
1805
  </html>