Potato의 커스텀 HTML 템플릿
HTML 템플릿, CSS 스타일링, JavaScript를 사용해 Potato에서 커스텀 어노테이션 인터페이스를 만드는 방법, 좌우 분할 레이아웃, 커스텀 위젯, 임베드된 미디어 표시까지 다룹니다.
Potato의 기본 제공 어노테이션 유형은 대부분의 작업을 다루지만, 때로는 원하는 인터페이스가 그중 어디에도 없는 경우가 있습니다. 커스텀 HTML 템플릿을 사용하면 직접 레이아웃을 작성하고, CSS를 넣고, 필요할 때 JavaScript를 연결할 수 있습니다. 이 가이드는 그런 템플릿을 만드는 과정을 안내합니다.
커스텀 템플릿이 필요한 경우
커스텀 템플릿은 기본 제공 유형이 필요한 레이아웃을 제공하지 못할 때 진가를 발휘합니다. 고유한 관례를 가진 의료나 법률 인터페이스, 인터랙티브 시각화, 일회성 입력 위젯, 또는 특정 브랜드에 맞춰야 하는 레이아웃 같은 경우입니다. 기본 제공 유형으로 이미 충분하다면 그것을 사용하십시오. 커스텀 템플릿은 유지보수해야 할 것이 더 많습니다.
기본 템플릿 구조
템플릿 설정
annotation_task_name: "Custom Template Annotation"
html_layout: "templates/my_template.html"템플릿 파일
<!-- 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>템플릿 변수
사용 가능한 변수
<!-- 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 -->커스텀 템플릿 스타일링
인라인 CSS
<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>JavaScript 통합
기본 인터랙티비티
<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>고급 템플릿
좌우 분할 비교
<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>인터랙티브 하이라이팅
<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>탭 인터페이스
<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>해두면 좋은 몇 가지
템플릿 자체는 얇게 유지하십시오. 로직이 스며들기 시작하면 원래 있어야 할 설정 쪽으로 다시 밀어 넣으십시오. 배포하기 전에 좁은 화면에서 테스트하십시오. 노트북에서는 괜찮아 보이는 그리드 레이아웃이 태블릿에서는 무너지기 때문입니다. 접근성의 기본을 챙기십시오. 읽기 쉬운 대비, 진짜 레이블, 키보드 내비게이션입니다. JavaScript와 이미지 크기는 적당히 다루십시오. 필드가 없는 경우 빈 공백을 렌더링하는 대신 그 상황을 처리하십시오. 그리고 다음 사람(아마 6개월 뒤의 본인일 것입니다)이 템플릿이 무엇을 하는지 알 수 있도록 주석을 한두 개 남겨 두십시오.
커스텀 레이아웃 뒤에 있는 설정 키에 대해서는 레이아웃 커스터마이징 및 폼 레이아웃 문서를 참고하십시오.
전체 문서는 /docs/core-concepts/annotation-schemes에서 확인할 수 있습니다.