Update index.html
Browse files- index.html +1804 -18
index.html
CHANGED
@@ -1,19 +1,1805 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, '&')
|
1642 |
+
.replace(/</g, '<')
|
1643 |
+
.replace(/>/g, '>')
|
1644 |
+
.replace(/"/g, '"')
|
1645 |
+
.replace(/'/g, ''');
|
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>
|