|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>JSON Schema Generator for Text Classification</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script> |
|
<style> |
|
.fade-in { |
|
animation: fadeIn 0.3s ease-in; |
|
} |
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(-10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
.tab-content { |
|
display: none; |
|
} |
|
.tab-content.active { |
|
display: block; |
|
} |
|
.copy-feedback { |
|
animation: copyPulse 2s ease-out; |
|
} |
|
@keyframes copyPulse { |
|
0% { opacity: 0; transform: translateY(10px); } |
|
20% { opacity: 1; transform: translateY(0); } |
|
80% { opacity: 1; transform: translateY(0); } |
|
100% { opacity: 0; transform: translateY(-10px); } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50"> |
|
<div class="container mx-auto px-4 py-8 max-w-6xl"> |
|
|
|
<div class="text-center mb-8"> |
|
<h1 class="text-3xl font-bold text-gray-800 mb-2">JSON Schema Generator for Text Classification</h1> |
|
<p class="text-gray-600">Generate JSON schemas for structured text classification with LLMs</p> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
|
|
|
<div class="space-y-6"> |
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6"> |
|
<h2 class="text-lg font-semibold mb-4">Classification Type</h2> |
|
<div class="space-y-3"> |
|
<label class="flex items-center cursor-pointer"> |
|
<input type="radio" name="classification-type" value="single" checked class="mr-3 text-blue-600"> |
|
<div> |
|
<span class="font-medium">Single Label</span> |
|
<span class="text-sm text-gray-500 ml-2">One label per text</span> |
|
</div> |
|
</label> |
|
<label class="flex items-center cursor-pointer"> |
|
<input type="radio" name="classification-type" value="multi" class="mr-3 text-blue-600"> |
|
<div> |
|
<span class="font-medium">Multi Label</span> |
|
<span class="text-sm text-gray-500 ml-2">Multiple labels allowed</span> |
|
</div> |
|
</label> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6"> |
|
<h2 class="text-lg font-semibold mb-4">Labels</h2> |
|
<p class="text-sm text-gray-600 mb-4">Add the categories you want to classify text into:</p> |
|
|
|
<div id="labels-container" class="space-y-2"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<details class="bg-white rounded-lg shadow-sm"> |
|
<summary class="p-6 cursor-pointer font-semibold">Advanced Options</summary> |
|
<div class="px-6 pb-6 space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Field Name</label> |
|
<input type="text" id="field-name" value="classification" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<p class="text-xs text-gray-500 mt-1">JSON key for the classification field</p> |
|
</div> |
|
|
|
<div> |
|
<label class="flex items-center cursor-pointer"> |
|
<input type="checkbox" id="is-required" checked class="mr-2 text-blue-600"> |
|
<span class="text-sm font-medium">Required Field</span> |
|
</label> |
|
</div> |
|
|
|
<div id="multi-options" class="space-y-4" style="display: none;"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Min Items</label> |
|
<input type="number" id="min-items" value="0" min="0" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Max Items</label> |
|
<input type="number" id="max-items" value="0" min="0" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<p class="text-xs text-gray-500 mt-1">0 = no limit</p> |
|
</div> |
|
</div> |
|
</div> |
|
</details> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6"> |
|
|
|
<div class="border-b border-gray-200 mb-4"> |
|
<nav class="-mb-px flex space-x-8"> |
|
<button class="tab-button py-2 px-1 border-b-2 font-medium text-sm border-blue-500 text-blue-600" data-tab="schema"> |
|
Schema |
|
</button> |
|
<button class="tab-button py-2 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="example"> |
|
Example |
|
</button> |
|
<button class="tab-button py-2 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="how-to-use"> |
|
How to Use |
|
</button> |
|
</nav> |
|
</div> |
|
|
|
|
|
<div id="schema" class="tab-content active"> |
|
<div class="mb-4"> |
|
<pre><code id="schema-output" class="language-json">// Please add at least one label to generate a schema</code></pre> |
|
</div> |
|
<div class="flex gap-2"> |
|
<button id="copy-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"> |
|
📋 Copy Schema |
|
</button> |
|
<button id="download-btn" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"> |
|
💾 Download |
|
</button> |
|
<div id="copy-feedback" class="ml-4 py-2 text-green-600 font-medium" style="display: none;"> |
|
✓ Copied to clipboard! |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="example" class="tab-content"> |
|
<pre><code id="example-output" class="language-json">// Example will appear here</code></pre> |
|
</div> |
|
|
|
<div id="how-to-use" class="tab-content prose prose-sm max-w-none"> |
|
<h3 class="text-lg font-semibold mb-3">Integration Guide</h3> |
|
|
|
<div class="mb-4"> |
|
<h4 class="font-medium mb-2">LM Studio:</h4> |
|
<ol class="list-decimal list-inside text-sm text-gray-700 space-y-1"> |
|
<li>Copy the generated schema</li> |
|
<li>Paste into the "Structured Output" field</li> |
|
<li>The model will only output valid JSON</li> |
|
</ol> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<h4 class="font-medium mb-2">OpenAI API:</h4> |
|
<pre class="bg-gray-100 p-3 rounded text-xs"><code>response_format = { |
|
"type": "json_schema", |
|
"json_schema": { |
|
"name": "classification", |
|
"schema": YOUR_SCHEMA_HERE |
|
} |
|
}</code></pre> |
|
</div> |
|
|
|
<div> |
|
<h4 class="font-medium mb-2">Other APIs:</h4> |
|
<ul class="list-disc list-inside text-sm text-gray-700 space-y-1"> |
|
<li>Use with any API supporting JSON Schema validation</li> |
|
<li>Check your API documentation for the parameter name</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let labels = []; |
|
let labelIdCounter = 0; |
|
|
|
|
|
function init() { |
|
addLabel('positive'); |
|
addLabel('negative'); |
|
updateSchema(); |
|
setupEventListeners(); |
|
} |
|
|
|
|
|
function addLabel(value = '') { |
|
const id = labelIdCounter++; |
|
labels.push({ id, value }); |
|
|
|
const container = document.getElementById('labels-container'); |
|
const labelDiv = document.createElement('div'); |
|
labelDiv.className = 'flex gap-2 fade-in'; |
|
labelDiv.id = `label-${id}`; |
|
|
|
labelDiv.innerHTML = ` |
|
<input type="text" |
|
value="${value}" |
|
placeholder="Enter label name" |
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" |
|
data-label-id="${id}"> |
|
<button onclick="addLabelAfter(${id})" |
|
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" |
|
title="Add label after this one"> |
|
+ |
|
</button> |
|
${labels.length > 2 ? ` |
|
<button onclick="removeLabel(${id})" |
|
class="px-3 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-md transition-colors" |
|
title="Remove this label"> |
|
× |
|
</button>` : ''} |
|
`; |
|
|
|
container.appendChild(labelDiv); |
|
|
|
|
|
const input = labelDiv.querySelector('input'); |
|
input.addEventListener('input', (e) => { |
|
const label = labels.find(l => l.id === id); |
|
if (label) { |
|
label.value = e.target.value; |
|
updateSchema(); |
|
|
|
|
|
const lastLabel = labels[labels.length - 1]; |
|
if (label.id === lastLabel.id && e.target.value.trim() !== '') { |
|
addLabel(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function addLabelAfter(afterId) { |
|
const index = labels.findIndex(l => l.id === afterId); |
|
const newId = labelIdCounter++; |
|
labels.splice(index + 1, 0, { id: newId, value: '' }); |
|
|
|
|
|
rebuildLabelsContainer(); |
|
updateSchema(); |
|
} |
|
|
|
|
|
function removeLabel(id) { |
|
if (labels.length <= 2) return; |
|
|
|
labels = labels.filter(l => l.id !== id); |
|
document.getElementById(`label-${id}`).remove(); |
|
updateSchema(); |
|
} |
|
|
|
|
|
function rebuildLabelsContainer() { |
|
const container = document.getElementById('labels-container'); |
|
container.innerHTML = ''; |
|
const currentLabels = [...labels]; |
|
labels = []; |
|
currentLabels.forEach(label => { |
|
addLabel(label.value); |
|
}); |
|
} |
|
|
|
|
|
function generateSchema() { |
|
const classificationType = document.querySelector('input[name="classification-type"]:checked').value; |
|
const fieldName = document.getElementById('field-name').value; |
|
const isRequired = document.getElementById('is-required').checked; |
|
const minItems = parseInt(document.getElementById('min-items').value) || 0; |
|
const maxItems = parseInt(document.getElementById('max-items').value) || 0; |
|
|
|
|
|
const validLabels = labels |
|
.map(l => l.value.trim()) |
|
.filter(v => v !== ''); |
|
|
|
if (validLabels.length === 0) { |
|
return { |
|
schema: '// Please add at least one label to generate a schema', |
|
example: '// Example will appear here' |
|
}; |
|
} |
|
|
|
|
|
if (new Set(validLabels).size !== validLabels.length) { |
|
return { |
|
schema: '// Error: Duplicate labels found. Each label must be unique.', |
|
example: '// Please fix duplicate labels' |
|
}; |
|
} |
|
|
|
|
|
const schema = { |
|
"$schema": "http://json-schema.org/draft-07/schema#", |
|
"type": "object", |
|
"properties": {} |
|
}; |
|
|
|
if (classificationType === 'single') { |
|
schema.properties[fieldName] = { |
|
"type": "string", |
|
"enum": validLabels, |
|
"description": `Classification into one of ${validLabels.length} categories` |
|
}; |
|
} else { |
|
schema.properties[fieldName] = { |
|
"type": "array", |
|
"items": { |
|
"type": "string", |
|
"enum": validLabels |
|
}, |
|
"description": `Multiple labels from ${validLabels.length} categories`, |
|
"uniqueItems": true |
|
}; |
|
|
|
if (minItems > 0) { |
|
schema.properties[fieldName].minItems = minItems; |
|
} |
|
if (maxItems > 0) { |
|
schema.properties[fieldName].maxItems = maxItems; |
|
} |
|
} |
|
|
|
if (isRequired) { |
|
schema.required = [fieldName]; |
|
} |
|
|
|
|
|
const example = {}; |
|
if (classificationType === 'single') { |
|
example[fieldName] = validLabels[0]; |
|
} else { |
|
example[fieldName] = validLabels.slice(0, 2); |
|
} |
|
|
|
return { |
|
schema: JSON.stringify(schema, null, 2), |
|
example: JSON.stringify(example, null, 2) |
|
}; |
|
} |
|
|
|
|
|
function updateSchema() { |
|
const result = generateSchema(); |
|
document.getElementById('schema-output').textContent = result.schema; |
|
document.getElementById('example-output').textContent = result.example; |
|
|
|
|
|
Prism.highlightAll(); |
|
} |
|
|
|
|
|
function setupEventListeners() { |
|
|
|
document.querySelectorAll('input[name="classification-type"]').forEach(radio => { |
|
radio.addEventListener('change', (e) => { |
|
document.getElementById('multi-options').style.display = |
|
e.target.value === 'multi' ? 'block' : 'none'; |
|
updateSchema(); |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('field-name').addEventListener('input', updateSchema); |
|
document.getElementById('is-required').addEventListener('change', updateSchema); |
|
document.getElementById('min-items').addEventListener('input', updateSchema); |
|
document.getElementById('max-items').addEventListener('input', updateSchema); |
|
|
|
|
|
document.querySelectorAll('.tab-button').forEach(button => { |
|
button.addEventListener('click', (e) => { |
|
|
|
document.querySelectorAll('.tab-button').forEach(b => { |
|
b.classList.remove('border-blue-500', 'text-blue-600'); |
|
b.classList.add('border-transparent', 'text-gray-500'); |
|
}); |
|
e.target.classList.remove('border-transparent', 'text-gray-500'); |
|
e.target.classList.add('border-blue-500', 'text-blue-600'); |
|
|
|
|
|
const tabName = e.target.dataset.tab; |
|
document.querySelectorAll('.tab-content').forEach(content => { |
|
content.classList.remove('active'); |
|
}); |
|
document.getElementById(tabName).classList.add('active'); |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('copy-btn').addEventListener('click', () => { |
|
const schema = document.getElementById('schema-output').textContent; |
|
navigator.clipboard.writeText(schema).then(() => { |
|
const feedback = document.getElementById('copy-feedback'); |
|
feedback.style.display = 'block'; |
|
feedback.classList.add('copy-feedback'); |
|
setTimeout(() => { |
|
feedback.style.display = 'none'; |
|
feedback.classList.remove('copy-feedback'); |
|
}, 2000); |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('download-btn').addEventListener('click', () => { |
|
const schema = document.getElementById('schema-output').textContent; |
|
const blob = new Blob([schema], { type: 'application/json' }); |
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = 'schema.json'; |
|
a.click(); |
|
URL.revokeObjectURL(url); |
|
}); |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init); |
|
</script> |
|
</body> |
|
</html> |