Template HTML personalizzati in Potato
Come costruire interfacce di annotazione su misura in Potato con template HTML, CSS e JavaScript: layout affiancati, widget personalizzati e contenuti multimediali incorporati.
I tipi di annotazione già pronti di Potato coprono quasi tutti i task, ma a volte l'interfaccia che ti serve non è tra quelli. I template HTML personalizzati ti lasciano scrivere il layout da zero, aggiungere CSS e, se serve, collegare del JavaScript. Questa guida ne costruisce uno passo per passo.
Quando conviene un template personalizzato
Un template personalizzato si ripaga quando i tipi predefiniti non riescono a darti il layout che ti serve: un'interfaccia medica o legale con convenzioni proprie, una visualizzazione interattiva, un widget di input creato per un caso solo, o un layout che deve seguire una precisa identità visiva. Se un tipo predefinito fa già il lavoro, usa quello. I template personalizzati costano di più in manutenzione.
Struttura di base di un template
Configurazione del template
annotation_task_name: "Custom Template Annotation"
html_layout: "templates/my_template.html"File del template
<!-- templates/my_template.html -->
<div class="custom-annotation-container">
<!-- Display the text -->
<div class="content-display">
<div class="text-content">
{{text}}
</div>
</div>
<!-- Custom annotation interface -->
<div class="annotation-area">
<!-- Potato will inject annotation schemes here -->
{{annotation_schemes}}
</div>
</div>Variabili del template
Variabili disponibili
<!-- Item data -->
{{id}} <!-- Item ID -->
{{text}} <!-- Main text content -->
{{image_url}} <!-- Image URL if present -->
{{audio_url}} <!-- Audio URL if present -->
<!-- Metadata -->
{{metadata.field_name}} <!-- Any metadata field -->Dare uno stile ai template personalizzati
CSS in linea
<style>
.custom-annotation-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.task-header {
background: linear-gradient(135deg, #6E56CF, #9F7AEA);
color: white;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.content-display {
background: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.text-content {
font-size: 16px;
line-height: 1.6;
color: #1E293B;
}
.metadata {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #E2E8F0;
font-size: 14px;
color: #64748B;
}
.annotation-area {
background: white;
border: 1px solid #E2E8F0;
border-radius: 8px;
padding: 20px;
}
/* Custom highlight styles */
.highlight-positive {
background-color: #D1FAE5;
padding: 2px 4px;
border-radius: 3px;
}
.highlight-negative {
background-color: #FEE2E2;
padding: 2px 4px;
border-radius: 3px;
}
</style>Integrazione con JavaScript
Interattività di base
<script>
document.addEventListener('DOMContentLoaded', function() {
// Get the text content element
const textContent = document.querySelector('.text-content');
// Add click-to-highlight functionality
textContent.addEventListener('mouseup', function() {
const selection = window.getSelection();
if (selection.toString().trim()) {
highlightSelection(selection);
}
});
function highlightSelection(selection) {
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.className = 'user-highlight';
range.surroundContents(span);
}
});
</script>Template avanzati
Confronto affiancato
<div class="comparison-container">
<style>
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.comparison-item {
border: 1px solid #E2E8F0;
border-radius: 8px;
padding: 15px;
}
.comparison-item.selected {
border-color: #6E56CF;
box-shadow: 0 0 0 2px rgba(110, 86, 207, 0.2);
}
.item-label {
font-weight: 600;
margin-bottom: 10px;
color: #6E56CF;
}
</style>
<div class="comparison-item" data-option="A" onclick="selectOption('A')">
<div class="item-label">Option A</div>
<div class="item-content">{{option_a}}</div>
</div>
<div class="comparison-item" data-option="B" onclick="selectOption('B')">
<div class="item-label">Option B</div>
<div class="item-content">{{option_b}}</div>
</div>
<script>
function selectOption(option) {
// Update visual selection
document.querySelectorAll('.comparison-item').forEach(el => {
el.classList.remove('selected');
});
document.querySelector(`[data-option="${option}"]`).classList.add('selected');
}
</script>
</div>Evidenziazione interattiva
<div class="highlight-annotation">
<style>
.highlight-toolbar {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.highlight-btn {
padding: 8px 16px;
border: 1px solid #E2E8F0;
border-radius: 6px;
cursor: pointer;
background: white;
}
.highlight-btn.active {
background: #6E56CF;
color: white;
border-color: #6E56CF;
}
.highlightable-text {
line-height: 1.8;
}
.highlight-positive { background: #D1FAE5; }
.highlight-negative { background: #FEE2E2; }
.highlight-neutral { background: #FEF3C7; }
</style>
<div class="highlight-toolbar">
<button class="highlight-btn" data-color="positive" onclick="setHighlightMode('positive')">
Positive
</button>
<button class="highlight-btn" data-color="negative" onclick="setHighlightMode('negative')">
Negative
</button>
<button class="highlight-btn" data-color="neutral" onclick="setHighlightMode('neutral')">
Neutral
</button>
<button class="highlight-btn" onclick="clearHighlights()">
Clear All
</button>
</div>
<div class="highlightable-text" id="textContent">
{{text}}
</div>
<script>
let currentMode = null;
let highlights = [];
function setHighlightMode(mode) {
currentMode = mode;
document.querySelectorAll('.highlight-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.color === mode);
});
}
document.getElementById('textContent').addEventListener('mouseup', function() {
if (!currentMode) return;
const selection = window.getSelection();
if (selection.toString().trim()) {
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.className = `highlight-${currentMode}`;
// Store highlight data
highlights.push({
text: selection.toString(),
type: currentMode,
start: range.startOffset,
end: range.endOffset
});
range.surroundContents(span);
selection.removeAllRanges();
}
});
function clearHighlights() {
highlights = [];
document.getElementById('textContent').innerHTML = '{{text}}';
}
</script>
</div>Interfaccia a schede
<div class="tabbed-interface">
<style>
.tab-buttons {
display: flex;
border-bottom: 2px solid #E2E8F0;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #64748B;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab-btn.active {
color: #6E56CF;
border-bottom-color: #6E56CF;
}
.tab-content {
display: none;
padding: 20px 0;
}
.tab-content.active {
display: block;
}
</style>
<div class="tab-buttons">
<button class="tab-btn active" onclick="showTab('text')">Text</button>
<button class="tab-btn" onclick="showTab('metadata')">Metadata</button>
<button class="tab-btn" onclick="showTab('context')">Context</button>
</div>
<div class="tab-content active" id="tab-text">
<div class="text-content">{{text}}</div>
</div>
<div class="tab-content" id="tab-metadata">
<pre>{{metadata | json}}</pre>
</div>
<div class="tab-content" id="tab-context">
<p><strong>Source:</strong> {{metadata.source}}</p>
<p><strong>Date:</strong> {{metadata.date}}</p>
<p><strong>Author:</strong> {{metadata.author}}</p>
</div>
<script>
function showTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
event.target.classList.add('active');
document.getElementById(`tab-${tabName}`).classList.add('active');
}
</script>
</div>Qualche accortezza utile
Tieni il template snello. Se ci si infila della logica, riportala nella configurazione, dove sta di casa. Prova su uno schermo stretto prima di mettere in produzione, perché i layout a griglia che sul portatile stanno bene su un tablet si sfaldano. Cura le basi dell'accessibilità: contrasto leggibile, etichette vere, navigazione da tastiera. Vai leggero con il JavaScript e con il peso delle immagini. Gestisci il caso in cui un campo manca, invece di lasciare uno spazio vuoto. E lascia un paio di commenti, così chi verrà dopo (con ogni probabilità tu, tra sei mesi) capisce che cosa fa il template.
Per le chiavi di configurazione dietro ai layout personalizzati, vedi la documentazione su layout customization e form layout.
Documentazione completa su Schemi di Annotazione.