Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>JSONL Dataset Maker</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Google Fonts Inter --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .code-font { | |
| font-family: 'Fira Code', monospace; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col selection:bg-indigo-500 selection:text-white"> | |
| <!-- Header --> | |
| <header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50 px-6 py-4"> | |
| <div class="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="h-10 w-10 rounded-xl bg-gradient-to-tr from-indigo-500 to-violet-500 flex items-center justify-center shadow-lg shadow-indigo-500/20"> | |
| <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold tracking-tight bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">JSONL Dataset Architect</h1> | |
| <p class="text-xs text-slate-400">Build high-quality multi-turn or single-turn AI training data</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <span class="text-xs bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-3 py-1.5 rounded-full font-medium"> | |
| Active Dataset Model | |
| </span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content Grid --> | |
| <main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| <!-- Left Column: Controls and Input Form (7 cols) --> | |
| <section class="lg:col-span-7 flex flex-col gap-6"> | |
| <!-- Format Selector Card --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl"> | |
| <label class="block text-sm font-semibold text-slate-300 mb-3">Choose Dataset Format</label> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> | |
| <div class="relative"> | |
| <select id="formatSelector" onchange="handleFormatChange()" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all cursor-pointer appearance-none"> | |
| <option value="reasoning">Reasoning Messages (with think tags)</option> | |
| <option value="messages">Messages (Normal, no reasoning)</option> | |
| <option value="sharegpt">ShareGPT Conversations</option> | |
| <option value="alpaca">Alpaca (Single Turn)</option> | |
| <option value="custom">Custom JSON Structure</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400"> | |
| <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg> | |
| </div> | |
| </div> | |
| <div id="formatBadge" class="flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center"> | |
| Multi-turn conversations supported | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Current Entry Editor Form --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex-1 flex flex-col gap-5"> | |
| <div class="flex justify-between items-center pb-4 border-b border-slate-800"> | |
| <h2 class="text-md font-bold text-slate-200 flex items-center gap-2"> | |
| <svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> | |
| </svg> | |
| Construct Current Line Entry | |
| </h2> | |
| <span id="turnsCount" class="text-xs bg-slate-800 px-2.5 py-1 rounded-md text-slate-400 font-medium"> | |
| 0 Turns Created | |
| </span> | |
| </div> | |
| <!-- Input Fields Container --> | |
| <div id="dynamicInputsContainer" class="flex-1 space-y-4 max-h-[50vh] overflow-y-auto pr-1"> | |
| <!-- Loaded dynamically via JavaScript based on Format Selector --> | |
| </div> | |
| <!-- Add Turn controls (hidden for Alpaca format) --> | |
| <div id="turnControls" class="flex flex-wrap gap-2 pt-2 border-t border-slate-800/50"> | |
| <button type="button" onclick="addTurn('user')" class="flex-1 min-w-[120px] bg-slate-800 hover:bg-slate-750 text-slate-200 hover:text-white px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all border border-slate-700/50"> | |
| <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m0-6H6" /> | |
| </svg> | |
| Add User Turn | |
| </button> | |
| <button type="button" onclick="addTurn('assistant')" class="flex-1 min-w-[120px] bg-slate-800 hover:bg-slate-750 text-slate-200 hover:text-white px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all border border-slate-700/50"> | |
| <svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m0-6H6" /> | |
| </svg> | |
| Add Assistant Turn | |
| </button> | |
| </div> | |
| <!-- Submission Controls --> | |
| <div class="flex gap-3 pt-3"> | |
| <button type="button" onclick="resetCurrentForm()" class="px-5 py-3 rounded-xl text-xs font-bold bg-slate-950 hover:bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 transition-all"> | |
| Reset Editor | |
| </button> | |
| <button type="button" onclick="commitEntry()" class="flex-1 bg-gradient-to-r from-indigo-500 to-violet-500 hover:from-indigo-600 hover:to-violet-600 text-white px-6 py-3 rounded-xl text-xs font-bold shadow-lg shadow-indigo-500/15 hover:shadow-indigo-500/25 transition-all flex items-center justify-center gap-2"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| Add Line to JSONL | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Right Column: Live Preview & Line Manager (5 cols) --> | |
| <section class="lg:col-span-5 flex flex-col gap-6"> | |
| <!-- Live Preview --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex flex-col h-[400px]"> | |
| <div class="flex justify-between items-center pb-4 border-b border-slate-800"> | |
| <div class="flex items-center gap-2"> | |
| <div class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></div> | |
| <h2 class="text-md font-bold text-slate-200">Live Preview</h2> | |
| </div> | |
| <button onclick="copyToClipboard()" class="bg-slate-800 hover:bg-indigo-600 hover:text-white text-slate-300 px-3 py-1.5 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all border border-slate-700/50"> | |
| <svg id="copyIcon" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> | |
| </svg> | |
| <span id="copyText">Copy Output</span> | |
| </button> | |
| </div> | |
| <!-- JSONL Preview Output --> | |
| <div class="flex-1 overflow-auto mt-4 bg-slate-950 border border-slate-850 rounded-xl p-4 relative"> | |
| <pre id="jsonlPreview" class="code-font text-xs text-indigo-300 whitespace-pre-wrap break-all select-all leading-relaxed h-full">{}</pre> | |
| <div id="emptyState" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-950 p-6 text-center"> | |
| <svg class="w-12 h-12 text-slate-700 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| <span class="text-sm font-semibold text-slate-500">No JSON Lines Created</span> | |
| <span class="text-xs text-slate-600 mt-1">Populate input fields and add a line to start building your dataset.</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- List Management --> | |
| <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex-1 flex flex-col min-h-[250px]"> | |
| <div class="flex justify-between items-center pb-4 border-b border-slate-800"> | |
| <h2 class="text-sm font-bold text-slate-200">Lines in Dataset</h2> | |
| <button onclick="clearDataset()" class="text-rose-400 hover:text-rose-300 text-xs font-semibold transition-all"> | |
| Clear All Lines | |
| </button> | |
| </div> | |
| <!-- Lines List Area --> | |
| <div id="datasetLinesContainer" class="flex-1 overflow-y-auto space-y-2 mt-4 max-h-[250px] pr-1"> | |
| <!-- Items mapped via JS --> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Custom Modal/Toast Notifications --> | |
| <div id="toast" class="fixed bottom-6 right-6 z-50 bg-slate-900 border border-slate-800 px-4 py-3 rounded-xl shadow-2xl flex items-center gap-3 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none"> | |
| <div id="toastIcon" class="h-6 w-6 rounded-full flex items-center justify-center"></div> | |
| <p id="toastMessage" class="text-xs font-medium text-slate-200"></p> | |
| </div> | |
| <!-- Hidden input helper for secure clipboard manipulation --> | |
| <textarea id="clipboardHelper" class="sr-only" aria-hidden="true"></textarea> | |
| <!-- App Logic Script --> | |
| <script> | |
| // Configuration options | |
| const APP_STATE = { | |
| currentFormat: 'reasoning', | |
| dataset: [], | |
| currentTurns: [], | |
| // Custom format structures | |
| customKeys: ['prompt', 'response'] | |
| }; | |
| // DOM elements | |
| const formatSelector = document.getElementById('formatSelector'); | |
| const formatBadge = document.getElementById('formatBadge'); | |
| const turnsCount = document.getElementById('turnsCount'); | |
| const dynamicInputsContainer = document.getElementById('dynamicInputsContainer'); | |
| const turnControls = document.getElementById('turnControls'); | |
| const jsonlPreview = document.getElementById('jsonlPreview'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const datasetLinesContainer = document.getElementById('datasetLinesContainer'); | |
| const clipboardHelper = document.getElementById('clipboardHelper'); | |
| const toastElement = document.getElementById('toast'); | |
| const toastMsg = document.getElementById('toastMessage'); | |
| const toastIcon = document.getElementById('toastIcon'); | |
| // Setup onload | |
| window.onload = function() { | |
| handleFormatChange(); | |
| }; | |
| function handleFormatChange() { | |
| APP_STATE.currentFormat = formatSelector.value; | |
| resetCurrentForm(); | |
| // Adjust metadata badge and controls based on selection | |
| if (APP_STATE.currentFormat === 'alpaca') { | |
| formatBadge.innerText = "Single-turn instruction data format"; | |
| formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center"; | |
| turnControls.classList.add('hidden'); | |
| turnsCount.classList.add('hidden'); | |
| buildAlpacaInputs(); | |
| } else if (APP_STATE.currentFormat === 'custom') { | |
| formatBadge.innerText = "Custom single-turn Key-Value mapping"; | |
| formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center"; | |
| turnControls.classList.add('hidden'); | |
| turnsCount.classList.add('hidden'); | |
| buildCustomInputs(); | |
| } else { | |
| formatBadge.innerText = "Multi-turn conversations supported"; | |
| formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center"; | |
| turnControls.classList.remove('hidden'); | |
| turnsCount.classList.remove('hidden'); | |
| // Add first turn default structure | |
| addTurn('system'); | |
| addTurn('user'); | |
| addTurn('assistant'); | |
| } | |
| } | |
| function addTurn(role) { | |
| const turnId = 'turn_' + Date.now() + '_' + Math.floor(Math.random() * 1000); | |
| const turnObj = { id: turnId, role: role, content: '' }; | |
| if (role === 'assistant' && APP_STATE.currentFormat === 'reasoning') { | |
| turnObj.reasoning = ''; | |
| } | |
| APP_STATE.currentTurns.push(turnObj); | |
| renderInputs(); | |
| updateTurnsBadge(); | |
| } | |
| function removeTurn(id) { | |
| APP_STATE.currentTurns = APP_STATE.currentTurns.filter(turn => turn.id !== id); | |
| renderInputs(); | |
| updateTurnsBadge(); | |
| } | |
| function updateTurnContent(id, field, value) { | |
| const index = APP_STATE.currentTurns.findIndex(turn => turn.id === id); | |
| if (index !== -1) { | |
| APP_STATE.currentTurns[index][field] = value; | |
| } | |
| } | |
| function updateTurnsBadge() { | |
| const userAndAssistantTurns = APP_STATE.currentTurns.filter(t => t.role !== 'system').length; | |
| turnsCount.innerText = `${userAndAssistantTurns} Turn${userAndAssistantTurns !== 1 ? 's' : ''} Created`; | |
| } | |
| function buildAlpacaInputs() { | |
| dynamicInputsContainer.innerHTML = ` | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Instruction</label> | |
| <textarea id="alp_instruction" placeholder="What should the AI do?" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[90px] transition-all resize-y"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Input (Optional context)</label> | |
| <textarea id="alp_input" placeholder="e.g. context text, variables, or background data" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[80px] transition-all resize-y"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Output</label> | |
| <textarea id="alp_output" placeholder="The golden response output" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[100px] transition-all resize-y"></textarea> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function buildCustomInputs() { | |
| let fieldsHtml = ''; | |
| APP_STATE.customKeys.forEach((key, index) => { | |
| fieldsHtml += ` | |
| <div class="flex items-center gap-3 bg-slate-950/40 p-3 rounded-xl border border-slate-850"> | |
| <div class="flex-1"> | |
| <input type="text" value="${key}" placeholder="Key name" onchange="updateCustomKeyName(${index}, this.value)" class="code-font bg-slate-950 border border-slate-800 rounded-lg px-2 py-1 text-xs text-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 mb-1.5 font-semibold"> | |
| <textarea id="custom_val_${index}" placeholder="Value content..." class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[60px] resize-y"></textarea> | |
| </div> | |
| <button type="button" onclick="removeCustomField(${index})" class="text-rose-500 hover:text-rose-400 hover:bg-slate-900 p-2 rounded-lg transition-all" title="Remove dynamic key"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| </div> | |
| `; | |
| }); | |
| dynamicInputsContainer.innerHTML = ` | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <label class="block text-xs font-bold text-slate-400 uppercase tracking-wide">Mapped Custom Keys</label> | |
| <button type="button" onclick="addCustomField()" class="text-indigo-400 hover:text-indigo-300 text-xs font-semibold flex items-center gap-1"> | |
| + Add Key Field | |
| </button> | |
| </div> | |
| <div class="space-y-3"> | |
| ${fieldsHtml} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function addCustomField() { | |
| const nextIndex = APP_STATE.customKeys.length + 1; | |
| APP_STATE.customKeys.push(`key_${nextIndex}`); | |
| buildCustomInputs(); | |
| } | |
| function removeCustomField(index) { | |
| APP_STATE.customKeys.splice(index, 1); | |
| buildCustomInputs(); | |
| } | |
| function updateCustomKeyName(index, newName) { | |
| const standardizedName = newName.trim().replace(/[^a-zA-Z0-9_]/g, ''); | |
| APP_STATE.customKeys[index] = standardizedName || `key_${index + 1}`; | |
| } | |
| function renderInputs() { | |
| if (APP_STATE.currentFormat === 'alpaca' || APP_STATE.currentFormat === 'custom') { | |
| return; // Managed individually | |
| } | |
| dynamicInputsContainer.innerHTML = ''; | |
| APP_STATE.currentTurns.forEach((turn, index) => { | |
| const isSystem = turn.role === 'system'; | |
| const isUser = turn.role === 'user' || turn.role === 'human'; | |
| // Set style accents based on role | |
| let roleLabel = turn.role.toUpperCase(); | |
| let borderTheme = 'border-slate-800'; | |
| let tagTheme = 'bg-slate-950 border border-slate-850 text-slate-400'; | |
| let inputPlaceholder = 'Enter system prompt instructions...'; | |
| if (isUser) { | |
| borderTheme = 'border-emerald-950/40 bg-emerald-950/5'; | |
| tagTheme = 'bg-emerald-950/40 text-emerald-400 border border-emerald-900/50'; | |
| inputPlaceholder = 'What would the human/user ask the model?'; | |
| } else if (turn.role === 'assistant' || turn.role === 'gpt') { | |
| borderTheme = 'border-indigo-950/40 bg-indigo-950/5'; | |
| tagTheme = 'bg-indigo-950/40 text-indigo-400 border border-indigo-900/50'; | |
| inputPlaceholder = 'Model response goes here...'; | |
| } | |
| // Compile reasoning fields html inside reasoning format assistant inputs | |
| let reasoningFieldHtml = ''; | |
| if (turn.role === 'assistant' && APP_STATE.currentFormat === 'reasoning') { | |
| reasoningFieldHtml = ` | |
| <div class="mt-2.5"> | |
| <label class="block text-[10px] font-semibold text-slate-500 mb-1">Reasoning Content (Wrapped inside think tags)</label> | |
| <textarea placeholder="Write the model reasoning trajectory..." oninput="updateTurnContent('${turn.id}', 'reasoning', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-indigo-300 placeholder-indigo-950 focus:outline-none focus:ring-1 focus:ring-indigo-500 min-h-[60px] resize-y">${turn.reasoning || ''}</textarea> | |
| </div> | |
| `; | |
| } | |
| const turnDiv = document.createElement('div'); | |
| turnDiv.className = `p-4 rounded-xl border ${borderTheme} flex flex-col gap-2 relative group transition-all`; | |
| turnDiv.innerHTML = ` | |
| <div class="flex justify-between items-center"> | |
| <span class="text-[10px] font-bold px-2.5 py-1 rounded-md ${tagTheme} tracking-wide uppercase"> | |
| ${roleLabel} | |
| </span> | |
| <button type="button" onclick="removeTurn('${turn.id}')" class="text-slate-600 hover:text-rose-400 hover:bg-slate-900 p-1 rounded-lg transition-all" title="Delete turn"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| </div> | |
| ${reasoningFieldHtml} | |
| <div class="mt-1"> | |
| <textarea placeholder="${inputPlaceholder}" oninput="updateTurnContent('${turn.id}', 'content', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 min-h-[70px] resize-y">${turn.content || ''}</textarea> | |
| </div> | |
| `; | |
| dynamicInputsContainer.appendChild(turnDiv); | |
| }); | |
| } | |
| function commitEntry() { | |
| let newEntry = null; | |
| if (APP_STATE.currentFormat === 'alpaca') { | |
| const instruction = document.getElementById('alp_instruction').value.trim(); | |
| const input = document.getElementById('alp_input').value.trim(); | |
| const output = document.getElementById('alp_output').value.trim(); | |
| if (!instruction || !output) { | |
| showToast('error', 'Instruction and Output are required fields'); | |
| return; | |
| } | |
| newEntry = { | |
| instruction: instruction, | |
| input: input, | |
| output: output | |
| }; | |
| } else if (APP_STATE.currentFormat === 'custom') { | |
| newEntry = {}; | |
| let validationError = false; | |
| APP_STATE.customKeys.forEach((key, index) => { | |
| const textVal = document.getElementById(`custom_val_${index}`).value; | |
| newEntry[key] = textVal; | |
| if (!key.trim()) validationError = true; | |
| }); | |
| if (validationError || APP_STATE.customKeys.length === 0) { | |
| showToast('error', 'Please configure proper key values first'); | |
| return; | |
| } | |
| } else { | |
| // Handling conversational formats: Messages, Reasoning, ShareGPT | |
| const list = []; | |
| let hasFilledTurn = false; | |
| APP_STATE.currentTurns.forEach(turn => { | |
| if (APP_STATE.currentFormat === 'sharegpt') { | |
| // Transform role for ShareGPT | |
| let sRole = 'human'; | |
| if (turn.role === 'system') sRole = 'system'; | |
| else if (turn.role === 'assistant') sRole = 'gpt'; | |
| list.push({ | |
| from: sRole, | |
| value: turn.content | |
| }); | |
| if (turn.content.trim()) hasFilledTurn = true; | |
| } else { | |
| // Messages & Reasoning | |
| let messageContent = turn.content; | |
| // If reasoning model format and role is assistant, prep prepended think tag | |
| if (APP_STATE.currentFormat === 'reasoning' && turn.role === 'assistant') { | |
| const rawReasoning = turn.reasoning || ''; | |
| messageContent = `<think>${rawReasoning}</think>\n${turn.content}`; | |
| } | |
| list.push({ | |
| role: turn.role, | |
| content: messageContent | |
| }); | |
| if (turn.content.trim()) hasFilledTurn = true; | |
| } | |
| }); | |
| if (!hasFilledTurn) { | |
| showToast('error', 'Cannot add empty turn array into dataset'); | |
| return; | |
| } | |
| if (APP_STATE.currentFormat === 'sharegpt') { | |
| newEntry = { conversations: list }; | |
| } else { | |
| newEntry = { messages: list }; | |
| } | |
| } | |
| // Append to array state | |
| APP_STATE.dataset.push(newEntry); | |
| updatePreview(); | |
| renderDatasetLinesList(); | |
| showToast('success', 'Entry added to dataset successfully'); | |
| // Keep system templates on dynamic inputs but flush current contents for fresh loop | |
| resetCurrentContentKeepStructure(); | |
| } | |
| function resetCurrentContentKeepStructure() { | |
| if (APP_STATE.currentFormat === 'alpaca') { | |
| document.getElementById('alp_instruction').value = ''; | |
| document.getElementById('alp_input').value = ''; | |
| document.getElementById('alp_output').value = ''; | |
| } else if (APP_STATE.currentFormat === 'custom') { | |
| APP_STATE.customKeys.forEach((_, index) => { | |
| const textarea = document.getElementById(`custom_val_${index}`); | |
| if (textarea) textarea.value = ''; | |
| }); | |
| } else { | |
| APP_STATE.currentTurns.forEach(turn => { | |
| turn.content = ''; | |
| if (turn.reasoning !== undefined) { | |
| turn.reasoning = ''; | |
| } | |
| }); | |
| renderInputs(); | |
| } | |
| } | |
| function resetCurrentForm() { | |
| APP_STATE.currentTurns = []; | |
| if (APP_STATE.currentFormat === 'alpaca') { | |
| buildAlpacaInputs(); | |
| } else if (APP_STATE.currentFormat === 'custom') { | |
| buildCustomInputs(); | |
| } else { | |
| addTurn('system'); | |
| addTurn('user'); | |
| addTurn('assistant'); | |
| } | |
| } | |
| function updatePreview() { | |
| if (APP_STATE.dataset.length === 0) { | |
| jsonlPreview.innerText = '{}'; | |
| emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| emptyState.classList.add('hidden'); | |
| const lines = APP_STATE.dataset.map(item => JSON.stringify(item)); | |
| jsonlPreview.innerText = lines.join('\n'); | |
| } | |
| function renderDatasetLinesList() { | |
| datasetLinesContainer.innerHTML = ''; | |
| APP_STATE.dataset.forEach((entry, idx) => { | |
| let entryHeader = `Line #${idx + 1}`; | |
| let entryDetails = ''; | |
| if (entry.messages) { | |
| const turnsCount = entry.messages.filter(m => m.role !== 'system').length; | |
| entryDetails = `${turnsCount} multi-turn message interaction(s)`; | |
| } else if (entry.conversations) { | |
| const turnsCount = entry.conversations.filter(m => m.from !== 'system').length; | |
| entryDetails = `${turnsCount} conversation exchange(s)`; | |
| } else if (entry.instruction) { | |
| entryDetails = 'Alpaca single-turn structured entry'; | |
| } else { | |
| entryDetails = 'Custom flat key-value dynamic format'; | |
| } | |
| const entryDiv = document.createElement('div'); | |
| entryDiv.className = "flex justify-between items-center bg-slate-950 p-3 rounded-xl border border-slate-850 hover:border-indigo-900/50 transition-all"; | |
| entryDiv.innerHTML = ` | |
| <div class="truncate pr-4"> | |
| <p class="text-xs font-bold text-slate-300">${entryHeader}</p> | |
| <p class="text-[10px] text-slate-500 truncate">${entryDetails}</p> | |
| </div> | |
| <button onclick="removeDatasetLine(${idx})" class="text-rose-500 hover:text-rose-400 hover:bg-slate-900 p-1.5 rounded-lg transition-all" title="Delete dataset line"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| `; | |
| datasetLinesContainer.appendChild(entryDiv); | |
| }); | |
| } | |
| function removeDatasetLine(index) { | |
| APP_STATE.dataset.splice(index, 1); | |
| updatePreview(); | |
| renderDatasetLinesList(); | |
| showToast('success', 'Dataset line removed'); | |
| } | |
| function clearDataset() { | |
| if (APP_STATE.dataset.length === 0) return; | |
| APP_STATE.dataset = []; | |
| updatePreview(); | |
| renderDatasetLinesList(); | |
| showToast('success', 'Dataset cleared completely'); | |
| } | |
| function copyToClipboard() { | |
| if (APP_STATE.dataset.length === 0) { | |
| showToast('error', 'Dataset is empty. Add entries first.'); | |
| return; | |
| } | |
| const rawText = jsonlPreview.innerText; | |
| clipboardHelper.value = rawText; | |
| clipboardHelper.select(); | |
| clipboardHelper.setSelectionRange(0, 99999); // Mobile compatibility | |
| try { | |
| document.execCommand('copy'); | |
| // Show copied UI indicator temporary swap | |
| const copyText = document.getElementById('copyText'); | |
| const copyIcon = document.getElementById('copyIcon'); | |
| copyText.innerText = 'Copied!'; | |
| copyIcon.innerHTML = ` | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| `; | |
| setTimeout(() => { | |
| copyText.innerText = 'Copy Output'; | |
| copyIcon.innerHTML = ` | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> | |
| `; | |
| }, 1500); | |
| showToast('success', 'JSONL output copied to clipboard!'); | |
| } catch (err) { | |
| showToast('error', 'Failed to copy text safely.'); | |
| } | |
| } | |
| function showToast(type, message) { | |
| // Apply theme config for toast icon | |
| if (type === 'success') { | |
| toastIcon.className = "h-6 w-6 rounded-full flex items-center justify-center bg-emerald-950 border border-emerald-900 text-emerald-400"; | |
| toastIcon.innerHTML = ` | |
| <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| `; | |
| } else { | |
| toastIcon.className = "h-6 w-6 rounded-full flex items-center justify-center bg-rose-950 border border-rose-900 text-rose-400"; | |
| toastIcon.innerHTML = ` | |
| <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| `; | |
| } | |
| toastMsg.innerText = message; | |
| // Pop Animation | |
| toastElement.classList.remove('opacity-0', 'translate-y-20', 'pointer-events-none'); | |
| setTimeout(() => { | |
| toastElement.classList.add('opacity-0', 'translate-y-20', 'pointer-events-none'); | |
| }, 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> |