wjbmattingly's picture
Update index.html
3cbfea9 verified
<!DOCTYPE html>
<html lang="en">
<!--
Latin Vulgate Text Editor with Marginalia
This standalone HTML file provides a sophisticated text editor for Latin texts with:
- Custom text input - paste your own Latin text for annotation
- Interactive text selection and semantic search
- Marginalia annotations linked to similar passages
- TEI XML export functionality
- Integration with Gradio app at https://medieval-data-latin-vulgate.hf.space
To use:
1. Serve this file from a web server (run: python serve.py)
2. Or deploy to any web hosting platform
3. Paste your own text or use the provided sample
4. Select text to find similar biblical passages
5. Export your annotated text as TEI XML
Dependencies: None - completely self-contained HTML/CSS/JavaScript
-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Latin Vulgate Text Editor with Marginalia</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Georgia', serif;
line-height: 1.6;
background-color: #f8f7f5;
color: #333;
}
.container {
display: grid;
grid-template-columns: 1fr 250px;
min-height: 100vh;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
gap: 20px;
}
.main-content {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow-y: auto;
position: relative;
}
.margin-area {
background: #f8f7f5;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 20px;
position: sticky;
top: 20px;
height: fit-content;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.margin-area h3 {
margin: 0 0 15px 0;
color: #2c3e50;
font-size: 1.1em;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
}
.margin-note {
background: white;
border: 1px solid #ddd;
border-left: 3px solid #3498db;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
font-size: 0.85em;
position: relative;
cursor: pointer;
}
.margin-note:hover {
background: #f8f9fa;
border-left-color: #2980b9;
}
.margin-note-reference {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.margin-note-text {
color: #555;
line-height: 1.3;
margin-bottom: 5px;
}
.margin-note-similarity {
font-size: 0.75em;
color: #777;
margin-bottom: 5px;
}
.margin-note-remove {
position: absolute;
top: 5px;
right: 5px;
background: #e74c3c;
color: white;
border: none;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.margin-note-remove:hover {
background: #c0392b;
}
.margin-note.highlighted {
background: #e8f4fd;
border-left-color: #3498db;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
}
.sidebar {
display: none; /* Hidden - marginalia now inline */
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.header h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
label {
font-weight: bold;
font-size: 0.9em;
color: #555;
}
select, input, button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
textarea#custom-text {
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
font-family: 'Georgia', serif;
font-size: 14px;
line-height: 1.4;
outline: none;
transition: border-color 0.3s;
resize: vertical;
}
textarea#custom-text:focus {
border-color: #3498db;
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
}
button {
background: #3498db;
color: white;
cursor: pointer;
border: none;
transition: background-color 0.3s;
}
button:hover {
background: #2980b9;
}
.export-btn {
background: #27ae60;
margin-left: auto;
}
.export-btn:hover {
background: #219a52;
}
.text-content {
font-size: 1.1em;
line-height: 1.8;
text-align: justify;
user-select: text;
cursor: text;
}
.text-content p {
margin-bottom: 20px;
text-indent: 2em;
}
.selected-text {
background-color: #ffffcc;
padding: 2px 4px;
border-radius: 3px;
position: relative;
}
.marginalia-marker {
background-color: #e8f4fd;
border-left: 3px solid #3498db;
padding: 2px 4px;
border-radius: 3px;
position: relative;
cursor: pointer;
}
.marginalia-indicator {
position: absolute;
top: -8px;
right: -8px;
background: #3498db;
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
z-index: 10;
}
.marginalia-indicator.multiple {
background: #e74c3c;
}
.marginalia-tooltip {
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
z-index: 1000;
max-width: 400px;
min-width: 300px;
display: none;
top: 100%;
left: 0;
margin-top: 5px;
font-size: 0.9em;
}
.marginalia-tooltip.show {
display: block;
}
.marginalia-tooltip-item {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.marginalia-tooltip-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.marginalia-tooltip-reference {
font-weight: bold;
color: #2c3e50;
margin-bottom: 6px;
font-size: 0.95em;
}
.marginalia-tooltip-text {
font-size: 0.9em;
line-height: 1.4;
margin-bottom: 6px;
color: #444;
}
.marginalia-tooltip-similarity {
font-size: 0.8em;
color: #666;
}
.popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 1000;
max-width: 90%;
max-height: 80%;
overflow-y: auto;
display: none;
}
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 999;
display: none;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.popup-header h3 {
margin: 0;
flex-shrink: 0;
}
.popup-header > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.close-btn {
background: #e74c3c;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.search-results {
max-height: 400px;
overflow-y: auto;
}
.result-item {
padding: 15px;
border: 1px solid #eee;
margin-bottom: 10px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f8f9fa;
}
.result-reference {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.result-text {
font-style: italic;
margin-bottom: 5px;
}
.result-similarity {
font-size: 0.9em;
color: #666;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
color: #e74c3c;
background: #fdf2f2;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
@media (max-width: 768px) {
.container {
padding: 10px;
grid-template-columns: 1fr; /* Stack on small screens */
gap: 10px;
}
.main-content {
padding: 20px;
}
.margin-area {
order: -1; /* Move marginalia above text on mobile */
position: static;
max-height: 300px;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.control-group {
width: 100%;
}
#custom-text {
min-width: auto !important;
width: 100% !important;
font-size: 16px; /* Prevent zoom on iOS */
border: 1px solid #ddd;
border-radius: 4px;
outline: none;
transition: border-color 0.3s;
}
#custom-text:focus {
border-color: #3498db;
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
}
.popup {
padding: 20px;
max-width: 95%;
}
.marginalia-tooltip {
max-width: 320px;
min-width: 280px;
font-size: 0.85em;
}
.margin-note {
font-size: 0.8em;
padding: 8px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="main-content">
<div class="header">
<h1>Latin Vulgate Quote Finder</h1>
<p>Paste your own text or use the sample below. Select text to search for similar passages and add marginalia</p>
<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;">
<strong>✅ API Configuration Fixed</strong><br>
The correct API endpoints have been identified. Your Gradio Space should now work properly with this interface.
<div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;">
<strong>Technical Details:</strong><br>
• Using proper Gradio API pattern: POST → GET with event streaming<br>
• Endpoint: <code>/gradio_api/call/predict</code><br>
• Your app.py has the correct <code>api_name="predict"</code> configuration
</div>
<div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;">
<strong>If you still see connection issues:</strong><br>
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>
</div>
</div>
<div id="connection-status" style="margin-top: 10px; padding: 8px; border-radius: 4px; font-size: 0.9em;">
<span id="status-indicator">🔄</span> <span id="status-text">Connecting to search backend...</span>
</div>
</div>
<div class="controls">
<div class="control-group">
<label for="custom-text">Paste Your Text:</label>
<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>
<div style="display: flex; gap: 10px; margin-top: 5px;">
<button onclick="loadCustomText()" style="background: #27ae60;">Load Text</button>
<button onclick="resetToSample()" style="background: #95a5a6;">Reset to Sample</button>
<button onclick="clearTextArea()" style="background: #e74c3c;">Clear</button>
</div>
</div>
<div class="control-group">
<label for="search-method">Search Method:</label>
<select id="search-method">
<option value="vector">Vector Search</option>
<option value="bm25">BM25 Search</option>
<option value="hybrid">Hybrid Search</option>
</select>
</div>
<div class="control-group">
<label for="result-limit">Results Limit:</label>
<input type="number" id="result-limit" min="1" max="50" value="10">
</div>
<div class="control-group">
<label for="book-filter">Filter by Books:</label>
<select id="book-filter" multiple style="height: 100px;">
<!-- Books will be populated dynamically -->
</select>
</div>
<button class="export-btn" onclick="exportToTEI()">Export as XML</button>
</div>
<div class="text-content" id="text-content">
<h2>De Imitatione Christi - Sample Text</h2>
<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>
<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>
<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>
<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>
<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>
</div>
</div>
<div class="margin-area">
<h3>Marginalia</h3>
<div id="marginalia-list">
<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>
</div>
</div>
</div>
<div class="popup-overlay" id="popup-overlay" onclick="closePopup()"></div>
<div class="popup" id="search-popup">
<div class="popup-header">
<div>
<h3>Similar Passages</h3>
<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>
</div>
<div>
<small style="color: #666; margin-right: 15px;">Click results to add marginalia</small>
<button class="close-btn" onclick="closePopup()">Close</button>
</div>
</div>
<div id="search-results" class="search-results">
<!-- Results will be populated here -->
</div>
</div>
<script>
let selectedText = '';
let selectedElement = null;
let marginalia = [];
let books = [];
let searchInProgress = false; // Flag to prevent clearing during search
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
loadBooks();
initializeTextSelection();
testGradioConnection();
initializeTextArea();
// Global click handler to hide marginalia tooltips
document.addEventListener('click', function(e) {
// Don't interfere with textarea interactions
if (e.target.closest('#custom-text')) {
return;
}
// Don't hide if clicking on a marginalia marker or its tooltip
if (e.target.closest('.marginalia-marker') || e.target.closest('.marginalia-tooltip')) {
return;
}
// Hide all open tooltips
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
tooltip.remove();
});
});
});
// Initialize textarea for proper keyboard handling
function initializeTextArea() {
const textArea = document.getElementById('custom-text');
// Ensure the textarea can receive focus and handle all keyboard events
textArea.setAttribute('tabindex', '0');
// Prevent event bubbling that might interfere with text editing
textArea.addEventListener('keydown', function(e) {
e.stopPropagation();
});
textArea.addEventListener('keyup', function(e) {
e.stopPropagation();
});
textArea.addEventListener('input', function(e) {
e.stopPropagation();
});
textArea.addEventListener('paste', function(e) {
e.stopPropagation();
});
textArea.addEventListener('cut', function(e) {
e.stopPropagation();
});
textArea.addEventListener('copy', function(e) {
e.stopPropagation();
});
// Ensure focus works properly
textArea.addEventListener('click', function(e) {
e.stopPropagation();
this.focus();
});
}
// Test connection to Gradio app
async function testGradioConnection() {
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
const statusDiv = document.getElementById('connection-status');
try {
console.log('Testing connection to Gradio app...');
// First, check if the space is accessible
const spaceCheck = await fetch('https://medieval-data-latin-vulgate.hf.space/', {
method: 'HEAD'
});
console.log('Space accessibility check:', spaceCheck.status);
// Test the proper Gradio API pattern
console.log('Testing Gradio API with /gradio_api/call/predict endpoint...');
// Step 1: POST to get event_id
const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: ["test", [], 1, "vector"]
})
});
console.log('POST response status:', postResponse.status);
if (!postResponse.ok) {
throw new Error(`API endpoint not available (${postResponse.status})`);
}
const postResult = await postResponse.json();
console.log('POST response:', postResult);
if (!postResult.event_id) {
throw new Error('Invalid API response - no event_id received');
}
console.log('Got event_id:', postResult.event_id);
// Step 2: Test GET endpoint (but don't wait for full response)
const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${postResult.event_id}`, {
method: 'GET',
});
console.log('GET response status:', getResponse.status);
if (getResponse.ok) {
statusIndicator.textContent = '✅';
statusText.textContent = 'Connected to search backend';
statusDiv.style.backgroundColor = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.style.border = '1px solid #c3e6cb';
console.log('API connection successful');
// Hide the API notice once connected
const apiNotice = document.getElementById('api-notice');
if (apiNotice) {
apiNotice.style.display = 'none';
}
} else {
throw new Error(`GET endpoint failed (${getResponse.status})`);
}
} catch (error) {
// Set up auto-retry every 30 seconds
statusIndicator.textContent = '🔄';
statusText.textContent = 'API not ready - will retry automatically...';
statusDiv.style.backgroundColor = '#fff3cd';
statusDiv.style.color = '#856404';
statusDiv.style.border = '1px solid #ffeaa7';
console.error('Gradio app connection test failed:', error);
setTimeout(() => {
console.log('Retrying API connection...');
testGradioConnection();
}, 30000); // Retry every 30 seconds
}
}
// Load available books from the Gradio app
async function loadBooks() {
try {
// Use the predefined book list since we can't easily get it from the Gradio API
const bookList = [
"Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth",
"1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra",
"Nehemiah", "Tobit", "Judith", "Esther", "1 Maccabees", "2 Maccabees", "Job", "Psalms",
"Proverbs", "Ecclesiastes", "Song of Solomon", "Wisdom", "Sirach", "Isaiah", "Jeremiah",
"Lamentations", "Baruch", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah",
"Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi",
"Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians",
"Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians",
"1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter",
"1 John", "2 John", "3 John", "Jude", "Revelation"
];
books = bookList;
const bookFilter = document.getElementById('book-filter');
books.forEach(book => {
const option = document.createElement('option');
option.value = book;
option.textContent = book;
bookFilter.appendChild(option);
});
} catch (error) {
console.error('Error loading books:', error);
}
}
// Initialize text selection functionality
function initializeTextSelection() {
const textContent = document.getElementById('text-content');
textContent.addEventListener('mouseup', function(e) {
const selection = window.getSelection();
if (selection.toString().trim().length > 0) {
selectedText = selection.toString().trim();
searchInProgress = true; // Set flag to prevent clearing
// Create a span around the selected text
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.className = 'selected-text';
try {
range.surroundContents(span);
selectedElement = span;
// Show search popup after a short delay
setTimeout(() => {
if (selectedText && searchInProgress) {
searchSimilarPassages(selectedText);
}
}, 100); // Reduced delay
} catch (error) {
// If surroundContents fails, clear selection
selection.removeAllRanges();
searchInProgress = false;
}
}
}
});
// Add click outside handler to clear temporary highlighting
document.addEventListener('click', function(e) {
// Don't interfere with textarea interactions
if (e.target.closest('#custom-text')) {
return;
}
// Don't clear if we're in the middle of a search
if (searchInProgress) {
return;
}
// Don't clear if clicking on popup or its contents
if (e.target.closest('#search-popup') || e.target.closest('.popup-overlay')) {
return;
}
// Don't clear if clicking on marginalia markers (they have their own tooltips)
if (e.target.closest('.marginalia-marker')) {
return;
}
// Don't clear if clicking on text content area (allow for text selection)
if (e.target.closest('#text-content') && window.getSelection().toString().trim().length > 0) {
return;
}
// Only clear if clicking outside the text content area
if (!e.target.closest('#text-content')) {
clearTemporarySelection();
}
});
}
// Clear temporary text selection (but keep marginalia markers)
function clearTemporarySelection() {
const tempSelected = document.querySelector('.selected-text');
if (tempSelected && !tempSelected.classList.contains('marginalia-marker')) {
const parent = tempSelected.parentNode;
while (tempSelected.firstChild) {
parent.insertBefore(tempSelected.firstChild, tempSelected);
}
parent.removeChild(tempSelected);
}
// Clear selection variables and reset flag
selectedText = '';
selectedElement = null;
searchInProgress = false;
window.getSelection().removeAllRanges();
}
// Parse HTML results from Gradio into JSON format
function parseGradioResults(htmlResult, query) {
try {
// Create a temporary DOM element to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlResult;
// Look for the table in the HTML
const table = tempDiv.querySelector('table');
if (!table) {
return [];
}
const rows = table.querySelectorAll('tbody tr');
const results = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
results.push({
reference: cells[0].textContent.trim(),
text: cells[1].innerHTML, // Keep HTML for highlighting
similarity: parseFloat(cells[2].textContent.trim()),
book: cells[3].textContent.trim(),
chapter: parseInt(cells[4].textContent.trim()),
verse: parseInt(cells[5].textContent.trim()),
raw_text: cells[1].textContent.trim() // Plain text version
});
}
});
return results;
} catch (error) {
console.error('Error parsing Gradio results:', error);
return [];
}
}
// Search for similar passages using Gradio API
async function searchSimilarPassages(query) {
const popup = document.getElementById('search-popup');
const overlay = document.getElementById('popup-overlay');
const results = document.getElementById('search-results');
const queryTextDiv = document.getElementById('query-text');
// Show popup with loading
results.innerHTML = '<div class="loading">Searching for similar passages...</div>';
popup.style.display = 'block';
overlay.style.display = 'block';
// Reset search flag once popup is shown
searchInProgress = false;
// Display the original query text
queryTextDiv.textContent = `Query: "${query}"`;
try {
const searchMethod = document.getElementById('search-method').value;
const resultLimit = parseInt(document.getElementById('result-limit').value);
const selectedBooks = Array.from(document.getElementById('book-filter').selectedOptions)
.map(option => option.value);
console.log(`Searching with query: "${query}"`);
// Step 1: POST to /gradio_api/call/predict to get event_id
const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: [query, selectedBooks, resultLimit, searchMethod]
})
});
if (!postResponse.ok) {
throw new Error(`POST request failed: ${postResponse.status} ${postResponse.statusText}`);
}
const postResult = await postResponse.json();
console.log('POST response:', postResult);
if (!postResult.event_id) {
throw new Error('No event_id received from server');
}
const eventId = postResult.event_id;
console.log('Got event_id:', eventId);
// Step 2: GET to /gradio_api/call/predict/{event_id} to stream results
const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${eventId}`, {
method: 'GET',
});
if (!getResponse.ok) {
throw new Error(`GET request failed: ${getResponse.status} ${getResponse.statusText}`);
}
// Parse Server-Sent Events stream
const reader = getResponse.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let finalResult = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
console.log('Received data:', data);
if (Array.isArray(data) && data.length > 0) {
finalResult = data[0]; // HTML result should be in first element
}
} catch (error) {
console.log('Error parsing data line:', error);
}
} else if (line.startsWith('event: ')) {
const eventType = line.slice(8);
console.log('Event type:', eventType);
if (eventType === 'complete' && finalResult) {
// We have the final result
const searchResults = parseGradioResults(finalResult, query);
displaySearchResults(searchResults, query);
return;
} else if (eventType === 'error') {
throw new Error('Server returned error event');
}
}
}
}
if (finalResult) {
const searchResults = parseGradioResults(finalResult, query);
displaySearchResults(searchResults, query);
} else {
throw new Error('No results received from server');
}
} catch (error) {
console.error('Error searching:', error);
results.innerHTML = `
<div class="error">
<strong>Search Error:</strong> ${error.message}<br><br>
<strong>Troubleshooting:</strong><br>
1. Check if your <a href="https://huggingface.co/spaces/medieval-data/latin-vulgate" target="_blank">HuggingFace Space</a> is running<br>
2. Try a "Factory restart" in Space settings<br>
3. Verify the Space has <code>api_name="predict"</code> in the Gradio interface<br>
4. Wait 2-3 minutes after restart for API to become available
</div>
`;
}
}
// Function removed - using direct Gradio API calls
// Display search results
function displaySearchResults(results, originalQuery) {
const resultsContainer = document.getElementById('search-results');
const queryTextDiv = document.getElementById('query-text');
// Make sure the query text is displayed
queryTextDiv.innerHTML = `<strong>Query:</strong> "${originalQuery}"`;
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="loading">No similar passages found.</div>';
return;
}
let html = '';
results.forEach((result, index) => {
const resultId = `result-${Date.now()}-${index}`;
html += `
<div class="result-item" data-result-id="${resultId}" data-original-query="${originalQuery}">
<div class="result-reference">${result.reference}</div>
<div class="result-text">${result.text}</div>
<div class="result-similarity">Similarity: ${result.similarity}</div>
</div>
`;
// Store result data globally for easy access
window.searchResults = window.searchResults || {};
window.searchResults[resultId] = {
reference: result.reference,
text: result.text,
similarity: result.similarity,
raw_text: result.raw_text || result.text
};
});
resultsContainer.innerHTML = html;
// Add click event listeners to result items
resultsContainer.querySelectorAll('.result-item').forEach(item => {
item.addEventListener('click', function() {
const resultId = this.getAttribute('data-result-id');
const originalQuery = this.getAttribute('data-original-query');
const resultData = window.searchResults[resultId];
if (resultData) {
addMarginalia(originalQuery, resultData);
}
});
});
}
// Add marginalia annotation
function addMarginalia(originalText, result) {
const marginaliaId = Date.now().toString();
// Add to marginalia array
marginalia.push({
id: marginaliaId,
originalText: originalText,
reference: result.reference,
text: result.raw_text,
similarity: result.similarity
});
// Update the selected text element
if (selectedElement) {
selectedElement.className = 'marginalia-marker';
selectedElement.setAttribute('data-marginalia-id', marginaliaId);
// Create or update marginalia indicator
updateMarginaliaIndicator(selectedElement);
// Add hover events for tooltip
setupMarginaliaHover(selectedElement);
}
// Add marginalia to the margin area
addMarginaliaToMargin(marginaliaId);
// Auto-close popup after adding marginalia
closePopup();
// Clear selection variables
selectedText = '';
selectedElement = null;
window.getSelection().removeAllRanges();
}
// Add marginalia to the margin area
function addMarginaliaToMargin(marginaliaId) {
const marginList = document.getElementById('marginalia-list');
const marginaliaItem = marginalia.find(item => item.id === marginaliaId);
if (!marginaliaItem) return;
// Remove placeholder text if present
const placeholder = marginList.querySelector('p');
if (placeholder) {
placeholder.remove();
}
// Create margin note
const marginNote = document.createElement('div');
marginNote.className = 'margin-note';
marginNote.setAttribute('data-marginalia-id', marginaliaId);
// Truncate text for margin display
const truncatedText = truncateText(marginaliaItem.text, 120);
marginNote.innerHTML = `
<div class="margin-note-reference">${marginaliaItem.reference}</div>
<div class="margin-note-text" title="${marginaliaItem.text}">${truncatedText}</div>
<div class="margin-note-similarity">Similarity: ${marginaliaItem.similarity}</div>
<button class="margin-note-remove" onclick="removeMarginalia('${marginaliaId}')" title="Remove marginalia">×</button>
`;
// Add hover effects to highlight corresponding text
marginNote.addEventListener('mouseenter', function() {
highlightCorrespondingText(marginaliaId, true);
});
marginNote.addEventListener('mouseleave', function() {
highlightCorrespondingText(marginaliaId, false);
});
// Add click to scroll to text
marginNote.addEventListener('click', function() {
scrollToText(marginaliaId);
});
marginList.appendChild(marginNote);
}
// Highlight corresponding text when hovering over margin note
function highlightCorrespondingText(marginaliaId, highlight) {
const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`);
const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`);
if (textMarker) {
if (highlight) {
textMarker.style.backgroundColor = '#ffffcc';
textMarker.style.outline = '2px solid #3498db';
} else {
textMarker.style.backgroundColor = '#e8f4fd';
textMarker.style.outline = '';
}
}
if (marginNote) {
if (highlight) {
marginNote.classList.add('highlighted');
} else {
marginNote.classList.remove('highlighted');
}
}
}
// Scroll to corresponding text when clicking margin note
function scrollToText(marginaliaId) {
const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`);
if (textMarker) {
textMarker.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// Temporarily highlight the text
highlightCorrespondingText(marginaliaId, true);
setTimeout(() => {
highlightCorrespondingText(marginaliaId, false);
}, 2000);
}
}
// Update marginalia indicator (shows count if multiple)
function updateMarginaliaIndicator(element) {
// Remove existing indicator
const existingIndicator = element.querySelector('.marginalia-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
// Get all marginalia for this element
const elementMarginalia = marginalia.filter(item =>
element.getAttribute('data-marginalia-id') === item.id ||
element.getAttribute('data-marginalia-ids')?.split(',').includes(item.id)
);
// Handle multiple marginalia on same text
const allIds = element.getAttribute('data-marginalia-id') ?
[element.getAttribute('data-marginalia-id')] : [];
if (elementMarginalia.length > 0) {
const indicator = document.createElement('div');
indicator.className = elementMarginalia.length > 1 ?
'marginalia-indicator multiple' : 'marginalia-indicator';
indicator.textContent = elementMarginalia.length > 1 ?
elementMarginalia.length.toString() : '•';
element.appendChild(indicator);
// Update data attribute for multiple marginalia
if (elementMarginalia.length > 1) {
element.setAttribute('data-marginalia-ids',
elementMarginalia.map(m => m.id).join(','));
}
}
}
// Setup hover events for marginalia tooltips
function setupMarginaliaHover(element) {
// Remove any existing listeners to avoid duplicates
element.removeEventListener('mouseenter', showMarginaliaTooltip);
element.removeEventListener('mouseleave', hideMarginaliaTooltip);
element.removeEventListener('click', showMarginaliaTooltip);
// Add both hover and click events for better usability
element.addEventListener('mouseenter', function(e) {
showMarginaliaTooltip(e);
// Also highlight corresponding margin notes
const marginaliaId = element.getAttribute('data-marginalia-id');
if (marginaliaId) {
highlightCorrespondingText(marginaliaId, true);
}
});
element.addEventListener('mouseleave', function(e) {
hideMarginaliaTooltip(e);
// Remove highlight from margin notes
const marginaliaId = element.getAttribute('data-marginalia-id');
if (marginaliaId) {
highlightCorrespondingText(marginaliaId, false);
}
});
element.addEventListener('click', function(e) {
e.stopPropagation();
showMarginaliaTooltip(e);
});
}
// Show marginalia tooltip on hover
function showMarginaliaTooltip(event) {
const element = event.target.closest('.marginalia-marker');
if (!element) return;
const marginaliaId = element.getAttribute('data-marginalia-id');
const marginaliaIds = element.getAttribute('data-marginalia-ids');
console.log('Showing tooltip for element:', element, 'ID:', marginaliaId, 'IDs:', marginaliaIds);
let relevantMarginalia = [];
if (marginaliaIds) {
// Multiple marginalia
const ids = marginaliaIds.split(',');
relevantMarginalia = marginalia.filter(item => ids.includes(item.id));
} else if (marginaliaId) {
// Single marginalia
relevantMarginalia = marginalia.filter(item => item.id === marginaliaId);
}
console.log('Relevant marginalia:', relevantMarginalia);
if (relevantMarginalia.length === 0) return;
// Remove any existing tooltip
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
tooltip.remove();
});
// Create tooltip
const tooltip = document.createElement('div');
tooltip.className = 'marginalia-tooltip show';
let tooltipHTML = '';
relevantMarginalia.forEach(item => {
tooltipHTML += `
<div class="marginalia-tooltip-item">
<div class="marginalia-tooltip-reference">${item.reference}</div>
<div class="marginalia-tooltip-text">${item.text}</div>
<div class="marginalia-tooltip-similarity">Similarity: ${item.similarity}</div>
</div>
`;
});
tooltip.innerHTML = tooltipHTML;
// Position tooltip relative to document body for better positioning
document.body.appendChild(tooltip);
// Position tooltip relative to the element
const rect = element.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let left = rect.left;
let top = rect.bottom + window.scrollY + 5;
// Adjust if tooltip would go off screen
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - tooltipRect.height - 5;
}
tooltip.style.position = 'absolute';
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
tooltip.style.zIndex = '9999';
console.log('Tooltip positioned at:', left, top);
// Store reference to the tooltip on the element for easy removal
element._tooltip = tooltip;
}
// Hide marginalia tooltip
function hideMarginaliaTooltip(event) {
const element = event.target.closest('.marginalia-marker');
if (!element) return;
// Remove the tooltip after a short delay to allow clicking on it
setTimeout(() => {
if (element._tooltip) {
element._tooltip.remove();
element._tooltip = null;
}
}, 100);
}
// Remove marginalia
function removeMarginalia(marginaliaId) {
console.log('Removing marginalia:', marginaliaId);
// Remove from array
marginalia = marginalia.filter(item => item.id !== marginaliaId);
// Remove from margin area
const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`);
if (marginNote) {
marginNote.remove();
}
// Add placeholder text if no marginalia left
const marginList = document.getElementById('marginalia-list');
if (marginalia.length === 0) {
marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>';
}
// Find the marker element
const marker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`) ||
document.querySelector(`[data-marginalia-ids*="${marginaliaId}"]`);
if (marker) {
// Handle multiple marginalia on same element
const marginaliaIds = marker.getAttribute('data-marginalia-ids');
if (marginaliaIds) {
// Multiple marginalia - remove this one and update
const remainingIds = marginaliaIds.split(',').filter(id => id !== marginaliaId);
if (remainingIds.length > 1) {
// Still multiple marginalia
marker.setAttribute('data-marginalia-ids', remainingIds.join(','));
updateMarginaliaIndicator(marker);
} else if (remainingIds.length === 1) {
// Only one left - convert back to single
marker.removeAttribute('data-marginalia-ids');
marker.setAttribute('data-marginalia-id', remainingIds[0]);
updateMarginaliaIndicator(marker);
} else {
// None left - remove marker entirely
const parent = marker.parentNode;
while (marker.firstChild) {
parent.insertBefore(marker.firstChild, marker);
}
parent.removeChild(marker);
}
} else {
// Single marginalia - remove marker entirely
const parent = marker.parentNode;
while (marker.firstChild) {
parent.insertBefore(marker.firstChild, marker);
}
parent.removeChild(marker);
}
// Clean up any existing tooltip
if (marker._tooltip) {
marker._tooltip.remove();
marker._tooltip = null;
}
}
// Hide any other open tooltips
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
tooltip.remove();
});
}
// Close popup
function closePopup() {
document.getElementById('search-popup').style.display = 'none';
document.getElementById('popup-overlay').style.display = 'none';
// Clear any temporary text selection when popup closes
clearTemporarySelection();
}
// Clear text selection
function clearSelection() {
clearTemporarySelection();
}
// Export to TEI XML
function exportToTEI() {
const textContent = document.getElementById('text-content').cloneNode(true);
// Remove control elements
const controls = textContent.querySelector('.controls');
if (controls) controls.remove();
// Get the clean HTML content
const cleanHtml = textContent.innerHTML;
// Generate TEI XML
const teiXml = generateTEIXML(cleanHtml);
// Create download
const blob = new Blob([teiXml], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vulgate-marginalia.xml';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Generate TEI XML format
function generateTEIXML(htmlContent) {
const timestamp = new Date().toISOString();
console.log('Original HTML content:', htmlContent);
// Create a temporary div to work with the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
// Remove control elements
const controls = tempDiv.querySelector('.controls');
if (controls) controls.remove();
console.log('HTML after removing controls:', tempDiv.innerHTML);
// Clean up HTML and convert to proper TEI structure
const cleanText = cleanHtmlToTei(tempDiv.innerHTML);
console.log('Cleaned TEI text:', cleanText);
console.log('Current marginalia array:', marginalia);
let teiHeader = `<?xml version="1.0" encoding="UTF-8"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
<teiHeader>
<fileDesc>
<titleStmt>
<title>Latin Vulgate Text with Marginalia</title>
<author>Generated by Vulgate Text Editor</author>
</titleStmt>
<publicationStmt>
<p>Generated on ${timestamp}</p>
</publicationStmt>
<sourceDesc>
<p>Latin Vulgate text with semantic similarity annotations</p>
</sourceDesc>
</fileDesc>
</teiHeader>
<text>
<body>
${cleanText}`;
// Add marginalia annotations in proper TEI format
if (marginalia.length > 0) {
teiHeader += '\n <div type="apparatus">\n';
marginalia.forEach(item => {
// Parse the reference to get book, chapter, verse
const refParts = parseReference(item.reference);
teiHeader += ` <note xml:id="${item.id}" type="parallel" resp="#semantic-analysis">
<bibl>
<title type="biblical-book">${refParts.book}</title>
<biblScope unit="chapter">${refParts.chapter}</biblScope>
<biblScope unit="verse">${refParts.verse}</biblScope>
</bibl>
<quote xml:lang="la">${escapeXml(item.text)}</quote>
<measure type="similarity" quantity="${item.similarity}"/>
</note>\n`;
});
teiHeader += ' </div>\n';
}
const teiFooter = ` </body>
</text>
</TEI>`;
const finalXml = teiHeader + teiFooter;
console.log('Final XML:', finalXml);
return finalXml;
}
// Clean HTML and convert to TEI structure
function cleanHtmlToTei(htmlContent) {
let teiContent = htmlContent;
console.log('Starting cleanHtmlToTei with:', teiContent);
// Convert headings first
teiContent = teiContent.replace(/<h2[^>]*>(.*?)<\/h2>/g, ' <head>$1</head>');
// Process marginalia markers - be more comprehensive with the regex patterns
// Handle markers with indicators (most comprehensive pattern)
let replacements = 0;
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div><\/span>/gs,
(match, id, text) => {
console.log(`Replacing marginalia marker ${id} with text: "${text}"`);
replacements++;
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
});
// Handle simpler marginalia markers without indicators
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<\/span>/gs,
(match, id, text) => {
console.log(`Replacing simple marginalia marker ${id} with text: "${text}"`);
replacements++;
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
});
// Alternative order - data-marginalia-id might come before class
teiContent = teiContent.replace(/<span[^>]*data-marginalia-id="([^"]*)"[^>]*class="marginalia-marker"[^>]*>(.*?)<\/span>/gs,
(match, id, text) => {
console.log(`Replacing alt-order marginalia marker ${id} with text: "${text}"`);
replacements++;
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`;
});
console.log(`Made ${replacements} marginalia replacements`);
// Remove any remaining HTML artifacts
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*>/g, '<seg>');
teiContent = teiContent.replace(/<\/span>/g, '</seg>');
teiContent = teiContent.replace(/<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div>/gs, '');
// Remove any style attributes and other HTML artifacts
teiContent = teiContent.replace(/style="[^"]*"/g, '');
teiContent = teiContent.replace(/class="[^"]*"/g, '');
teiContent = teiContent.replace(/data-[^=]*="[^"]*"/g, '');
// Clean up paragraphs - add proper indentation while preserving content
teiContent = teiContent.replace(/<p>/g, ' <p>');
teiContent = teiContent.replace(/<\/p>/g, '</p>');
// Remove empty paragraphs
teiContent = teiContent.replace(/\s*<p>\s*<\/p>\s*/g, '');
// Clean up extra whitespace but preserve text flow
teiContent = teiContent.replace(/\s+>/g, '>');
teiContent = teiContent.replace(/>\s+</g, '><');
// Ensure proper line breaks between elements
teiContent = teiContent.replace(/(<\/p>)(?=\s*<)/g, '$1\n');
teiContent = teiContent.replace(/(<\/head>)(?=\s*<)/g, '$1\n');
// Final cleanup - normalize whitespace
teiContent = teiContent.trim();
console.log('Final cleaned TEI content:', teiContent);
return teiContent;
}
// Parse biblical reference into components
function parseReference(reference) {
// Handle references like "Jo 8:12", "1Cor 13:4", "Mt 5:3-4"
const match = reference.match(/^(\d?\s*\w+)\s+(\d+):(\d+)(?:-\d+)?$/);
if (match) {
return {
book: match[1].trim(),
chapter: match[2],
verse: match[3]
};
}
// Fallback for non-standard formats
return {
book: reference,
chapter: "1",
verse: "1"
};
}
// Escape XML special characters
function escapeXml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Utility function to truncate text
function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
// Find the last space before the max length to avoid cutting words
const truncated = text.substring(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
if (lastSpace > maxLength * 0.8) {
return truncated.substring(0, lastSpace) + '...';
}
return truncated + '...';
}
// Function to load custom text into the editor
function loadCustomText() {
const customTextArea = document.getElementById('custom-text');
const textContent = document.getElementById('text-content');
if (customTextArea.value.trim()) {
const inputText = customTextArea.value.trim();
// Clear any existing marginalia when loading new text
clearAllMarginalia();
// Format the text properly
let formattedText = '';
// Check if input already contains HTML
if (inputText.includes('<') && inputText.includes('>')) {
// Input appears to be HTML - use as is but ensure proper structure
formattedText = inputText;
} else {
// Plain text - convert to proper HTML paragraphs
// Split by double line breaks for paragraphs
const paragraphs = inputText.split(/\n\s*\n/);
// Add a default title if none present
if (!inputText.toLowerCase().includes('<h') && paragraphs.length > 0) {
formattedText = '<h2>Custom Text</h2>\n';
}
// Convert each paragraph
paragraphs.forEach(para => {
const cleanPara = para.replace(/\n/g, ' ').trim();
if (cleanPara) {
formattedText += `<p>${cleanPara}</p>\n`;
}
});
}
textContent.innerHTML = formattedText;
// Clear the textarea
customTextArea.value = '';
// Re-initialize text selection
initializeTextSelection();
// Show success message
showStatusMessage('Custom text loaded successfully!', 'success');
} else {
showStatusMessage('Please paste your text into the textarea first.', 'error');
}
}
// Function to clear all marginalia
function clearAllMarginalia() {
marginalia = [];
const marginList = document.getElementById('marginalia-list');
marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>';
// Remove all marginalia markers from text
document.querySelectorAll('.marginalia-marker').forEach(marker => {
const parent = marker.parentNode;
while (marker.firstChild) {
parent.insertBefore(marker.firstChild, marker);
}
parent.removeChild(marker);
});
// Clear any open tooltips
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => {
tooltip.remove();
});
}
// Function to show status messages
function showStatusMessage(message, type = 'info') {
const existingMessage = document.getElementById('status-message');
if (existingMessage) {
existingMessage.remove();
}
const messageDiv = document.createElement('div');
messageDiv.id = 'status-message';
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
font-weight: bold;
z-index: 10000;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
${type === 'success' ? 'background: #27ae60;' : ''}
${type === 'error' ? 'background: #e74c3c;' : ''}
${type === 'info' ? 'background: #3498db;' : ''}
`;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
// Auto-remove after 3 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, 3000);
}
// Function to reset text to sample
function resetToSample() {
const textContent = document.getElementById('text-content');
textContent.innerHTML = `
<h2>De Imitatione Christi - Sample Text</h2>
<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>
<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>
<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>
<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>
<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>
`;
// Clear any existing marginalia when resetting
clearAllMarginalia();
// Re-initialize text selection
initializeTextSelection();
// Show success message
showStatusMessage('Reset to sample text successfully!', 'success');
}
// Function to clear the custom text area
function clearTextArea() {
const customTextArea = document.getElementById('custom-text');
customTextArea.value = '';
showStatusMessage('Custom text area cleared.', 'info');
}
</script>
</body>
</html>