Commit
·
161b93c
1
Parent(s):
169dab9
Rewrite as clean HTML/JS app with dynamic label management
Browse files- Replaced buggy Gradio implementation with pure HTML/JS
- Dynamic label addition/removal with inline controls
- Clean, modern UI using Tailwind CSS
- Real-time JSON schema generation
- Syntax highlighting with Prism.js
- Tabbed interface for better organization
- Copy to clipboard and download functionality
- Starts with 2 labels, auto-expands as needed
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
- README.md +126 -7
- examples/multi_label_tags.json +29 -0
- examples/sentiment_analysis.json +12 -0
- examples/topic_classification.json +23 -0
- index.html +432 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -1,12 +1,131 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: JSON Schema Generator for Text Classification
|
| 3 |
+
emoji: 🏷️
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
+
sdk: static
|
| 7 |
+
app_file: index.html
|
|
|
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# JSON Schema Generator for Text Classification
|
| 13 |
+
|
| 14 |
+
A simple, user-friendly tool to generate JSON schemas for text classification tasks. Perfect for structured generation with Large Language Models (LLMs).
|
| 15 |
+
|
| 16 |
+
## 🎯 Purpose
|
| 17 |
+
|
| 18 |
+
This tool helps you create properly formatted JSON schemas that can be used with:
|
| 19 |
+
- **LM Studio** - Structured Output field
|
| 20 |
+
- **OpenAI API** - `response_format` parameter
|
| 21 |
+
- **llama.cpp** - Grammar constraints
|
| 22 |
+
- **Any LLM API** supporting JSON schema validation
|
| 23 |
+
|
| 24 |
+
## 🚀 Features
|
| 25 |
+
|
| 26 |
+
- **Single-label classification** - Choose one category from a list
|
| 27 |
+
- **Multi-label classification** - Select multiple applicable categories
|
| 28 |
+
- **Live preview** - See your schema and example outputs in real-time
|
| 29 |
+
- **Copy & Download** - Easy export options for your schemas
|
| 30 |
+
- **Beginner-friendly** - No JSON knowledge required
|
| 31 |
+
|
| 32 |
+
## 📖 How to Use
|
| 33 |
+
|
| 34 |
+
1. **Select Classification Type**
|
| 35 |
+
- **Single**: For mutually exclusive categories (e.g., sentiment: positive OR negative)
|
| 36 |
+
- **Multi**: For multiple applicable labels (e.g., topics: can be both "sports" AND "politics")
|
| 37 |
+
|
| 38 |
+
2. **Add Your Labels**
|
| 39 |
+
- Enter the categories you want to classify text into
|
| 40 |
+
- Add optional descriptions for clarity
|
| 41 |
+
- Use the ➕ button to add more labels
|
| 42 |
+
|
| 43 |
+
3. **Configure Options** (Optional)
|
| 44 |
+
- Change the field name (default: "classification")
|
| 45 |
+
- Set whether the field is required
|
| 46 |
+
- For multi-label: set min/max number of selections
|
| 47 |
+
|
| 48 |
+
4. **Get Your Schema**
|
| 49 |
+
- Copy the generated JSON schema
|
| 50 |
+
- Download as a .json file
|
| 51 |
+
- See example outputs
|
| 52 |
+
|
| 53 |
+
## 🔧 Example Use Cases
|
| 54 |
+
|
| 55 |
+
### Sentiment Analysis
|
| 56 |
+
```json
|
| 57 |
+
{
|
| 58 |
+
"type": "object",
|
| 59 |
+
"properties": {
|
| 60 |
+
"sentiment": {
|
| 61 |
+
"type": "string",
|
| 62 |
+
"enum": ["positive", "negative", "neutral"]
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"required": ["sentiment"]
|
| 66 |
+
}
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### Topic Classification (Multi-label)
|
| 70 |
+
```json
|
| 71 |
+
{
|
| 72 |
+
"type": "object",
|
| 73 |
+
"properties": {
|
| 74 |
+
"topics": {
|
| 75 |
+
"type": "array",
|
| 76 |
+
"items": {
|
| 77 |
+
"type": "string",
|
| 78 |
+
"enum": ["technology", "health", "finance", "education", "entertainment"]
|
| 79 |
+
},
|
| 80 |
+
"uniqueItems": true
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## 🤝 Integration Examples
|
| 87 |
+
|
| 88 |
+
### LM Studio
|
| 89 |
+
1. Generate your schema using this tool
|
| 90 |
+
2. Copy the schema
|
| 91 |
+
3. In LM Studio, paste into the "Structured Output" field
|
| 92 |
+
4. The model will only generate JSON matching your schema
|
| 93 |
+
|
| 94 |
+
### OpenAI API (Python)
|
| 95 |
+
```python
|
| 96 |
+
import openai
|
| 97 |
+
|
| 98 |
+
# Your generated schema
|
| 99 |
+
response_format = {
|
| 100 |
+
"type": "json_schema",
|
| 101 |
+
"json_schema": {
|
| 102 |
+
"name": "classification",
|
| 103 |
+
"schema": { ... } # Paste your schema here
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
response = openai.chat.completions.create(
|
| 108 |
+
model="gpt-4",
|
| 109 |
+
messages=[...],
|
| 110 |
+
response_format=response_format
|
| 111 |
+
)
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## 📚 Resources
|
| 115 |
+
|
| 116 |
+
- [JSON Schema Documentation](https://json-schema.org/)
|
| 117 |
+
- [OpenAI Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)
|
| 118 |
+
- [LM Studio Documentation](https://lmstudio.ai/docs)
|
| 119 |
+
|
| 120 |
+
## 🛠️ Technical Details
|
| 121 |
+
|
| 122 |
+
This tool generates JSON Schema Draft 7 compatible schemas that enforce:
|
| 123 |
+
- Valid JSON structure
|
| 124 |
+
- Type constraints (string, array)
|
| 125 |
+
- Enum restrictions for valid label values
|
| 126 |
+
- Array uniqueness for multi-label classification
|
| 127 |
+
- Optional min/max constraints for array lengths
|
| 128 |
+
|
| 129 |
+
## 📝 License
|
| 130 |
+
|
| 131 |
+
MIT License - Feel free to use and modify!
|
examples/multi_label_tags.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"type": "object",
|
| 4 |
+
"properties": {
|
| 5 |
+
"tags": {
|
| 6 |
+
"type": "array",
|
| 7 |
+
"items": {
|
| 8 |
+
"type": "string",
|
| 9 |
+
"enum": [
|
| 10 |
+
"urgent",
|
| 11 |
+
"important",
|
| 12 |
+
"bug",
|
| 13 |
+
"feature",
|
| 14 |
+
"documentation",
|
| 15 |
+
"question",
|
| 16 |
+
"help-wanted",
|
| 17 |
+
"good-first-issue",
|
| 18 |
+
"enhancement",
|
| 19 |
+
"wontfix"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
"description": "Multiple labels from 10 categories",
|
| 23 |
+
"uniqueItems": true,
|
| 24 |
+
"minItems": 1,
|
| 25 |
+
"maxItems": 5
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"required": ["tags"]
|
| 29 |
+
}
|
examples/sentiment_analysis.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"type": "object",
|
| 4 |
+
"properties": {
|
| 5 |
+
"sentiment": {
|
| 6 |
+
"type": "string",
|
| 7 |
+
"enum": ["positive", "negative", "neutral"],
|
| 8 |
+
"description": "Classification into one of 3 categories"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"required": ["sentiment"]
|
| 12 |
+
}
|
examples/topic_classification.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"type": "object",
|
| 4 |
+
"properties": {
|
| 5 |
+
"topic": {
|
| 6 |
+
"type": "string",
|
| 7 |
+
"enum": [
|
| 8 |
+
"technology",
|
| 9 |
+
"health",
|
| 10 |
+
"finance",
|
| 11 |
+
"education",
|
| 12 |
+
"entertainment",
|
| 13 |
+
"sports",
|
| 14 |
+
"politics",
|
| 15 |
+
"science",
|
| 16 |
+
"business",
|
| 17 |
+
"lifestyle"
|
| 18 |
+
],
|
| 19 |
+
"description": "Classification into one of 10 categories"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"required": ["topic"]
|
| 23 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>JSON Schema Generator for Text Classification</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
| 10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
.fade-in {
|
| 13 |
+
animation: fadeIn 0.3s ease-in;
|
| 14 |
+
}
|
| 15 |
+
@keyframes fadeIn {
|
| 16 |
+
from { opacity: 0; transform: translateY(-10px); }
|
| 17 |
+
to { opacity: 1; transform: translateY(0); }
|
| 18 |
+
}
|
| 19 |
+
.tab-content {
|
| 20 |
+
display: none;
|
| 21 |
+
}
|
| 22 |
+
.tab-content.active {
|
| 23 |
+
display: block;
|
| 24 |
+
}
|
| 25 |
+
.copy-feedback {
|
| 26 |
+
animation: copyPulse 2s ease-out;
|
| 27 |
+
}
|
| 28 |
+
@keyframes copyPulse {
|
| 29 |
+
0% { opacity: 0; transform: translateY(10px); }
|
| 30 |
+
20% { opacity: 1; transform: translateY(0); }
|
| 31 |
+
80% { opacity: 1; transform: translateY(0); }
|
| 32 |
+
100% { opacity: 0; transform: translateY(-10px); }
|
| 33 |
+
}
|
| 34 |
+
</style>
|
| 35 |
+
</head>
|
| 36 |
+
<body class="bg-gray-50">
|
| 37 |
+
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
| 38 |
+
<!-- Header -->
|
| 39 |
+
<div class="text-center mb-8">
|
| 40 |
+
<h1 class="text-3xl font-bold text-gray-800 mb-2">JSON Schema Generator for Text Classification</h1>
|
| 41 |
+
<p class="text-gray-600">Generate JSON schemas for structured text classification with LLMs</p>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 45 |
+
<!-- Left Column - Configuration -->
|
| 46 |
+
<div class="space-y-6">
|
| 47 |
+
<!-- Classification Type -->
|
| 48 |
+
<div class="bg-white rounded-lg shadow-sm p-6">
|
| 49 |
+
<h2 class="text-lg font-semibold mb-4">Classification Type</h2>
|
| 50 |
+
<div class="space-y-3">
|
| 51 |
+
<label class="flex items-center cursor-pointer">
|
| 52 |
+
<input type="radio" name="classification-type" value="single" checked class="mr-3 text-blue-600">
|
| 53 |
+
<div>
|
| 54 |
+
<span class="font-medium">Single Label</span>
|
| 55 |
+
<span class="text-sm text-gray-500 ml-2">One label per text</span>
|
| 56 |
+
</div>
|
| 57 |
+
</label>
|
| 58 |
+
<label class="flex items-center cursor-pointer">
|
| 59 |
+
<input type="radio" name="classification-type" value="multi" class="mr-3 text-blue-600">
|
| 60 |
+
<div>
|
| 61 |
+
<span class="font-medium">Multi Label</span>
|
| 62 |
+
<span class="text-sm text-gray-500 ml-2">Multiple labels allowed</span>
|
| 63 |
+
</div>
|
| 64 |
+
</label>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- Labels -->
|
| 69 |
+
<div class="bg-white rounded-lg shadow-sm p-6">
|
| 70 |
+
<h2 class="text-lg font-semibold mb-4">Labels</h2>
|
| 71 |
+
<p class="text-sm text-gray-600 mb-4">Add the categories you want to classify text into:</p>
|
| 72 |
+
|
| 73 |
+
<div id="labels-container" class="space-y-2">
|
| 74 |
+
<!-- Labels will be dynamically added here -->
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<!-- Advanced Options -->
|
| 79 |
+
<details class="bg-white rounded-lg shadow-sm">
|
| 80 |
+
<summary class="p-6 cursor-pointer font-semibold">Advanced Options</summary>
|
| 81 |
+
<div class="px-6 pb-6 space-y-4">
|
| 82 |
+
<div>
|
| 83 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Field Name</label>
|
| 84 |
+
<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">
|
| 85 |
+
<p class="text-xs text-gray-500 mt-1">JSON key for the classification field</p>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div>
|
| 89 |
+
<label class="flex items-center cursor-pointer">
|
| 90 |
+
<input type="checkbox" id="is-required" checked class="mr-2 text-blue-600">
|
| 91 |
+
<span class="text-sm font-medium">Required Field</span>
|
| 92 |
+
</label>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div id="multi-options" class="space-y-4" style="display: none;">
|
| 96 |
+
<div>
|
| 97 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Min Items</label>
|
| 98 |
+
<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">
|
| 99 |
+
</div>
|
| 100 |
+
<div>
|
| 101 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Max Items</label>
|
| 102 |
+
<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">
|
| 103 |
+
<p class="text-xs text-gray-500 mt-1">0 = no limit</p>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</details>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<!-- Right Column - Output -->
|
| 111 |
+
<div class="bg-white rounded-lg shadow-sm p-6">
|
| 112 |
+
<!-- Tabs -->
|
| 113 |
+
<div class="border-b border-gray-200 mb-4">
|
| 114 |
+
<nav class="-mb-px flex space-x-8">
|
| 115 |
+
<button class="tab-button py-2 px-1 border-b-2 font-medium text-sm border-blue-500 text-blue-600" data-tab="schema">
|
| 116 |
+
Schema
|
| 117 |
+
</button>
|
| 118 |
+
<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">
|
| 119 |
+
Example
|
| 120 |
+
</button>
|
| 121 |
+
<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">
|
| 122 |
+
How to Use
|
| 123 |
+
</button>
|
| 124 |
+
</nav>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<!-- Tab Contents -->
|
| 128 |
+
<div id="schema" class="tab-content active">
|
| 129 |
+
<div class="mb-4">
|
| 130 |
+
<pre><code id="schema-output" class="language-json">// Please add at least one label to generate a schema</code></pre>
|
| 131 |
+
</div>
|
| 132 |
+
<div class="flex gap-2">
|
| 133 |
+
<button id="copy-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
|
| 134 |
+
📋 Copy Schema
|
| 135 |
+
</button>
|
| 136 |
+
<button id="download-btn" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors">
|
| 137 |
+
💾 Download
|
| 138 |
+
</button>
|
| 139 |
+
<div id="copy-feedback" class="ml-4 py-2 text-green-600 font-medium" style="display: none;">
|
| 140 |
+
✓ Copied to clipboard!
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div id="example" class="tab-content">
|
| 146 |
+
<pre><code id="example-output" class="language-json">// Example will appear here</code></pre>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<div id="how-to-use" class="tab-content prose prose-sm max-w-none">
|
| 150 |
+
<h3 class="text-lg font-semibold mb-3">Integration Guide</h3>
|
| 151 |
+
|
| 152 |
+
<div class="mb-4">
|
| 153 |
+
<h4 class="font-medium mb-2">LM Studio:</h4>
|
| 154 |
+
<ol class="list-decimal list-inside text-sm text-gray-700 space-y-1">
|
| 155 |
+
<li>Copy the generated schema</li>
|
| 156 |
+
<li>Paste into the "Structured Output" field</li>
|
| 157 |
+
<li>The model will only output valid JSON</li>
|
| 158 |
+
</ol>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<div class="mb-4">
|
| 162 |
+
<h4 class="font-medium mb-2">OpenAI API:</h4>
|
| 163 |
+
<pre class="bg-gray-100 p-3 rounded text-xs"><code>response_format = {
|
| 164 |
+
"type": "json_schema",
|
| 165 |
+
"json_schema": {
|
| 166 |
+
"name": "classification",
|
| 167 |
+
"schema": YOUR_SCHEMA_HERE
|
| 168 |
+
}
|
| 169 |
+
}</code></pre>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div>
|
| 173 |
+
<h4 class="font-medium mb-2">Other APIs:</h4>
|
| 174 |
+
<ul class="list-disc list-inside text-sm text-gray-700 space-y-1">
|
| 175 |
+
<li>Use with any API supporting JSON Schema validation</li>
|
| 176 |
+
<li>Check your API documentation for the parameter name</li>
|
| 177 |
+
</ul>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<script>
|
| 185 |
+
// State management
|
| 186 |
+
let labels = [];
|
| 187 |
+
let labelIdCounter = 0;
|
| 188 |
+
|
| 189 |
+
// Initialize with 2 labels
|
| 190 |
+
function init() {
|
| 191 |
+
addLabel('positive');
|
| 192 |
+
addLabel('negative');
|
| 193 |
+
updateSchema();
|
| 194 |
+
setupEventListeners();
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Add a new label
|
| 198 |
+
function addLabel(value = '') {
|
| 199 |
+
const id = labelIdCounter++;
|
| 200 |
+
labels.push({ id, value });
|
| 201 |
+
|
| 202 |
+
const container = document.getElementById('labels-container');
|
| 203 |
+
const labelDiv = document.createElement('div');
|
| 204 |
+
labelDiv.className = 'flex gap-2 fade-in';
|
| 205 |
+
labelDiv.id = `label-${id}`;
|
| 206 |
+
|
| 207 |
+
labelDiv.innerHTML = `
|
| 208 |
+
<input type="text"
|
| 209 |
+
value="${value}"
|
| 210 |
+
placeholder="Enter label name"
|
| 211 |
+
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
| 212 |
+
data-label-id="${id}">
|
| 213 |
+
<button onclick="addLabelAfter(${id})"
|
| 214 |
+
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
| 215 |
+
title="Add label after this one">
|
| 216 |
+
+
|
| 217 |
+
</button>
|
| 218 |
+
${labels.length > 2 ? `
|
| 219 |
+
<button onclick="removeLabel(${id})"
|
| 220 |
+
class="px-3 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-md transition-colors"
|
| 221 |
+
title="Remove this label">
|
| 222 |
+
×
|
| 223 |
+
</button>` : ''}
|
| 224 |
+
`;
|
| 225 |
+
|
| 226 |
+
container.appendChild(labelDiv);
|
| 227 |
+
|
| 228 |
+
// Add event listener to the input
|
| 229 |
+
const input = labelDiv.querySelector('input');
|
| 230 |
+
input.addEventListener('input', (e) => {
|
| 231 |
+
const label = labels.find(l => l.id === id);
|
| 232 |
+
if (label) {
|
| 233 |
+
label.value = e.target.value;
|
| 234 |
+
updateSchema();
|
| 235 |
+
|
| 236 |
+
// Auto-add new label if typing in the last one
|
| 237 |
+
const lastLabel = labels[labels.length - 1];
|
| 238 |
+
if (label.id === lastLabel.id && e.target.value.trim() !== '') {
|
| 239 |
+
addLabel();
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
});
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// Add label after specific position
|
| 246 |
+
function addLabelAfter(afterId) {
|
| 247 |
+
const index = labels.findIndex(l => l.id === afterId);
|
| 248 |
+
const newId = labelIdCounter++;
|
| 249 |
+
labels.splice(index + 1, 0, { id: newId, value: '' });
|
| 250 |
+
|
| 251 |
+
// Rebuild the container
|
| 252 |
+
rebuildLabelsContainer();
|
| 253 |
+
updateSchema();
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// Remove a label
|
| 257 |
+
function removeLabel(id) {
|
| 258 |
+
if (labels.length <= 2) return;
|
| 259 |
+
|
| 260 |
+
labels = labels.filter(l => l.id !== id);
|
| 261 |
+
document.getElementById(`label-${id}`).remove();
|
| 262 |
+
updateSchema();
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Rebuild labels container
|
| 266 |
+
function rebuildLabelsContainer() {
|
| 267 |
+
const container = document.getElementById('labels-container');
|
| 268 |
+
container.innerHTML = '';
|
| 269 |
+
const currentLabels = [...labels];
|
| 270 |
+
labels = [];
|
| 271 |
+
currentLabels.forEach(label => {
|
| 272 |
+
addLabel(label.value);
|
| 273 |
+
});
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// Generate JSON schema
|
| 277 |
+
function generateSchema() {
|
| 278 |
+
const classificationType = document.querySelector('input[name="classification-type"]:checked').value;
|
| 279 |
+
const fieldName = document.getElementById('field-name').value;
|
| 280 |
+
const isRequired = document.getElementById('is-required').checked;
|
| 281 |
+
const minItems = parseInt(document.getElementById('min-items').value) || 0;
|
| 282 |
+
const maxItems = parseInt(document.getElementById('max-items').value) || 0;
|
| 283 |
+
|
| 284 |
+
// Get valid labels
|
| 285 |
+
const validLabels = labels
|
| 286 |
+
.map(l => l.value.trim())
|
| 287 |
+
.filter(v => v !== '');
|
| 288 |
+
|
| 289 |
+
if (validLabels.length === 0) {
|
| 290 |
+
return {
|
| 291 |
+
schema: '// Please add at least one label to generate a schema',
|
| 292 |
+
example: '// Example will appear here'
|
| 293 |
+
};
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Check for duplicates
|
| 297 |
+
if (new Set(validLabels).size !== validLabels.length) {
|
| 298 |
+
return {
|
| 299 |
+
schema: '// Error: Duplicate labels found. Each label must be unique.',
|
| 300 |
+
example: '// Please fix duplicate labels'
|
| 301 |
+
};
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Build schema
|
| 305 |
+
const schema = {
|
| 306 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 307 |
+
"type": "object",
|
| 308 |
+
"properties": {}
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
+
if (classificationType === 'single') {
|
| 312 |
+
schema.properties[fieldName] = {
|
| 313 |
+
"type": "string",
|
| 314 |
+
"enum": validLabels,
|
| 315 |
+
"description": `Classification into one of ${validLabels.length} categories`
|
| 316 |
+
};
|
| 317 |
+
} else {
|
| 318 |
+
schema.properties[fieldName] = {
|
| 319 |
+
"type": "array",
|
| 320 |
+
"items": {
|
| 321 |
+
"type": "string",
|
| 322 |
+
"enum": validLabels
|
| 323 |
+
},
|
| 324 |
+
"description": `Multiple labels from ${validLabels.length} categories`,
|
| 325 |
+
"uniqueItems": true
|
| 326 |
+
};
|
| 327 |
+
|
| 328 |
+
if (minItems > 0) {
|
| 329 |
+
schema.properties[fieldName].minItems = minItems;
|
| 330 |
+
}
|
| 331 |
+
if (maxItems > 0) {
|
| 332 |
+
schema.properties[fieldName].maxItems = maxItems;
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
if (isRequired) {
|
| 337 |
+
schema.required = [fieldName];
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// Generate example
|
| 341 |
+
const example = {};
|
| 342 |
+
if (classificationType === 'single') {
|
| 343 |
+
example[fieldName] = validLabels[0];
|
| 344 |
+
} else {
|
| 345 |
+
example[fieldName] = validLabels.slice(0, 2);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
return {
|
| 349 |
+
schema: JSON.stringify(schema, null, 2),
|
| 350 |
+
example: JSON.stringify(example, null, 2)
|
| 351 |
+
};
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
// Update schema display
|
| 355 |
+
function updateSchema() {
|
| 356 |
+
const result = generateSchema();
|
| 357 |
+
document.getElementById('schema-output').textContent = result.schema;
|
| 358 |
+
document.getElementById('example-output').textContent = result.example;
|
| 359 |
+
|
| 360 |
+
// Re-highlight syntax
|
| 361 |
+
Prism.highlightAll();
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Setup event listeners
|
| 365 |
+
function setupEventListeners() {
|
| 366 |
+
// Classification type change
|
| 367 |
+
document.querySelectorAll('input[name="classification-type"]').forEach(radio => {
|
| 368 |
+
radio.addEventListener('change', (e) => {
|
| 369 |
+
document.getElementById('multi-options').style.display =
|
| 370 |
+
e.target.value === 'multi' ? 'block' : 'none';
|
| 371 |
+
updateSchema();
|
| 372 |
+
});
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
// Other inputs
|
| 376 |
+
document.getElementById('field-name').addEventListener('input', updateSchema);
|
| 377 |
+
document.getElementById('is-required').addEventListener('change', updateSchema);
|
| 378 |
+
document.getElementById('min-items').addEventListener('input', updateSchema);
|
| 379 |
+
document.getElementById('max-items').addEventListener('input', updateSchema);
|
| 380 |
+
|
| 381 |
+
// Tab switching
|
| 382 |
+
document.querySelectorAll('.tab-button').forEach(button => {
|
| 383 |
+
button.addEventListener('click', (e) => {
|
| 384 |
+
// Update active tab
|
| 385 |
+
document.querySelectorAll('.tab-button').forEach(b => {
|
| 386 |
+
b.classList.remove('border-blue-500', 'text-blue-600');
|
| 387 |
+
b.classList.add('border-transparent', 'text-gray-500');
|
| 388 |
+
});
|
| 389 |
+
e.target.classList.remove('border-transparent', 'text-gray-500');
|
| 390 |
+
e.target.classList.add('border-blue-500', 'text-blue-600');
|
| 391 |
+
|
| 392 |
+
// Show corresponding content
|
| 393 |
+
const tabName = e.target.dataset.tab;
|
| 394 |
+
document.querySelectorAll('.tab-content').forEach(content => {
|
| 395 |
+
content.classList.remove('active');
|
| 396 |
+
});
|
| 397 |
+
document.getElementById(tabName).classList.add('active');
|
| 398 |
+
});
|
| 399 |
+
});
|
| 400 |
+
|
| 401 |
+
// Copy button
|
| 402 |
+
document.getElementById('copy-btn').addEventListener('click', () => {
|
| 403 |
+
const schema = document.getElementById('schema-output').textContent;
|
| 404 |
+
navigator.clipboard.writeText(schema).then(() => {
|
| 405 |
+
const feedback = document.getElementById('copy-feedback');
|
| 406 |
+
feedback.style.display = 'block';
|
| 407 |
+
feedback.classList.add('copy-feedback');
|
| 408 |
+
setTimeout(() => {
|
| 409 |
+
feedback.style.display = 'none';
|
| 410 |
+
feedback.classList.remove('copy-feedback');
|
| 411 |
+
}, 2000);
|
| 412 |
+
});
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
// Download button
|
| 416 |
+
document.getElementById('download-btn').addEventListener('click', () => {
|
| 417 |
+
const schema = document.getElementById('schema-output').textContent;
|
| 418 |
+
const blob = new Blob([schema], { type: 'application/json' });
|
| 419 |
+
const url = URL.createObjectURL(blob);
|
| 420 |
+
const a = document.createElement('a');
|
| 421 |
+
a.href = url;
|
| 422 |
+
a.download = 'schema.json';
|
| 423 |
+
a.click();
|
| 424 |
+
URL.revokeObjectURL(url);
|
| 425 |
+
});
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
// Initialize on load
|
| 429 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 430 |
+
</script>
|
| 431 |
+
</body>
|
| 432 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# No requirements needed for static HTML app
|