Templates HTML Personalizados no Potato
Crie interfaces de anotação personalizadas no Potato usando templates HTML, estilização CSS e JavaScript, layouts lado a lado, widgets personalizados e exibições de mídia incorporadas.
Os tipos de anotação integrados do Potato cobrem a maioria das tarefas, mas às vezes a interface que você quer simplesmente não é uma delas. Templates HTML personalizados permitem que você escreva seu próprio layout, insira CSS e conecte JavaScript quando precisar. Este guia mostra como criar um.
Quando recorrer a um template personalizado
Um template personalizado vale a pena quando os tipos integrados não conseguem oferecer o layout de que você precisa: uma interface médica ou jurídica com suas próprias convenções, uma visualização interativa, um widget de entrada pontual ou um layout que precisa combinar com uma marca específica. Se um tipo integrado já dá conta do recado, use-o. Templates personalizados dão mais trabalho de manter.
Estrutura básica do template
Configuração do template
annotation_task_name: "Custom Template Annotation"
html_layout: "templates/my_template.html"Arquivo do 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>Variáveis do template
Variáveis disponíveis
<!-- 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 -->Estilizando templates personalizados
CSS inline
<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>Integração com JavaScript
Interatividade básica
<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>Templates avançados
Comparação lado a lado
<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>Destaque interativo
<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>Interface com abas
<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>Algumas coisas que vale a pena fazer
Mantenha o template em si enxuto. Se a lógica começar a se infiltrar, devolva-a para a configuração, onde ela deve estar. Teste em uma tela estreita antes de publicar, porque layouts em grade que parecem bons em um notebook se desmontam em um tablet. Cuide do básico de acessibilidade: contraste legível, rótulos de verdade, navegação por teclado. Vá com calma no JavaScript e no tamanho das imagens. Trate o caso em que um campo está ausente em vez de renderizar uma lacuna em branco. E deixe um comentário ou dois para que a próxima pessoa (provavelmente você, daqui a seis meses) consiga entender o que o template faz.
Para as chaves de configuração por trás dos layouts personalizados, consulte a documentação de customização de layout e layout de formulário.
Documentação completa em /docs/core-concepts/annotation-schemes.