| <!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> |