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