Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Dataset Converter | Finetuning Assistant</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.dropzone { | |
border: 2px dashed #cbd5e0; | |
transition: all 0.3s ease; | |
} | |
.dropzone.active { | |
border-color: #4f46e5; | |
background-color: #f0f4ff; | |
} | |
.dropzone.error { | |
border-color: #ef4444; | |
background-color: #fef2f2; | |
} | |
.dropzone.success { | |
border-color: #10b981; | |
background-color: #ecfdf5; | |
} | |
.preset-card { | |
transition: all 0.2s ease; | |
} | |
.preset-card:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
} | |
.preset-card.selected { | |
border-color: #4f46e5; | |
background-color: #f0f4ff; | |
} | |
.header-input:focus { | |
outline: none; | |
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.5); | |
} | |
.smooth-transition { | |
transition: all 0.3s ease; | |
} | |
.preview-container { | |
max-height: 300px; | |
overflow-y: auto; | |
} | |
.preview-container::-webkit-scrollbar { | |
width: 6px; | |
} | |
.preview-container::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
.preview-container::-webkit-scrollbar-thumb { | |
background: #888; | |
border-radius: 3px; | |
} | |
.preview-container::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
.progress-bar { | |
height: 4px; | |
background-color: #e5e7eb; | |
border-radius: 2px; | |
overflow: hidden; | |
} | |
.progress-bar-fill { | |
height: 100%; | |
background-color: #4f46e5; | |
transition: width 0.3s ease; | |
} | |
.file-info { | |
display: none; | |
} | |
.spinner { | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.transform-section { | |
display: none; | |
} | |
.transform-section.active { | |
display: block; | |
} | |
.tab-button { | |
transition: all 0.2s ease; | |
} | |
.tab-button.active { | |
border-bottom: 2px solid #4f46e5; | |
color: #4f46e5; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8 max-w-6xl"> | |
<!-- Header --> | |
<header class="mb-10 text-center"> | |
<h1 class="text-4xl font-bold text-indigo-700 mb-2">Dataset Converter</h1> | |
<p class="text-gray-600 text-lg">Quickly transform datasets for AI fine-tuning with one-click presets</p> | |
</header> | |
<!-- Main Content --> | |
<div class="bg-white rounded-xl shadow-lg overflow-hidden"> | |
<!-- Step Navigation --> | |
<div class="flex border-b border-gray-200"> | |
<div class="w-1/3 py-4 px-6 text-center border-b-2 border-indigo-500 font-medium text-indigo-600"> | |
<i class="fas fa-upload mr-2"></i> Upload Data | |
</div> | |
<div class="w-1/3 py-4 px-6 text-center border-b-2 border-gray-200 font-medium text-gray-500"> | |
<i class="fas fa-magic mr-2"></i> Transform | |
</div> | |
<div class="w-1/3 py-4 px-6 text-center border-b-2 border-gray-200 font-medium text-gray-500"> | |
<i class="fas fa-download mr-2"></i> Download | |
</div> | |
</div> | |
<!-- Step 1: Upload Data --> | |
<div id="step1" class="p-8"> | |
<div class="mb-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-3">Upload Your Dataset</h2> | |
<p class="text-gray-600">Supported formats: CSV, JSON, Excel, TSV (Max 10MB)</p> | |
</div> | |
<div id="dropzone" class="dropzone rounded-lg p-12 text-center cursor-pointer mb-4"> | |
<div id="upload-icon" class="mx-auto w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4"> | |
<i class="fas fa-cloud-upload-alt text-indigo-500 text-2xl"></i> | |
</div> | |
<h3 id="dropzone-title" class="text-lg font-medium text-gray-700 mb-1">Drag & drop your file here</h3> | |
<p id="dropzone-subtitle" class="text-gray-500 mb-4">or</p> | |
<label for="file-upload" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"> | |
<i class="fas fa-folder-open mr-2"></i> Browse Files | |
<input id="file-upload" type="file" class="hidden" accept=".csv,.json,.xlsx,.xls,.tsv"> | |
</label> | |
<div id="progress-container" class="mt-4 hidden"> | |
<div class="progress-bar"> | |
<div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%"></div> | |
</div> | |
<p id="progress-text" class="text-sm text-gray-500 mt-1">Uploading...</p> | |
</div> | |
<div id="file-info" class="file-info mt-4 p-3 bg-gray-50 rounded-lg"> | |
<div class="flex items-center justify-between"> | |
<div class="flex items-center truncate"> | |
<i id="file-icon" class="fas fa-file-alt text-indigo-500 mr-2"></i> | |
<span id="file-name" class="text-sm font-medium text-gray-700 truncate"></span> | |
</div> | |
<button id="remove-file" class="text-red-500 hover:text-red-700"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div id="file-meta" class="text-xs text-gray-500 mt-1"></div> | |
</div> | |
<div id="error-message" class="hidden mt-4 p-3 bg-red-50 text-red-600 rounded-lg text-sm"></div> | |
</div> | |
<div class="flex justify-between items-center"> | |
<div class="text-sm text-gray-500"> | |
<i class="fas fa-info-circle mr-1"></i> Your data remains private and is processed locally. | |
</div> | |
<button id="next-step-1" class="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
Next <i class="fas fa-arrow-right ml-2"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Step 2: Transform Data --> | |
<div id="step2" class="p-8 hidden"> | |
<div class="mb-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-3">Transform Your Dataset</h2> | |
<p class="text-gray-600">Choose a preset or customize the transformation</p> | |
</div> | |
<!-- Transformation Tabs --> | |
<div class="flex border-b border-gray-200 mb-6"> | |
<button class="tab-button active px-4 py-2 font-medium text-sm focus:outline-none" data-tab="presets"> | |
<i class="fas fa-bolt mr-2"></i> Quick Presets | |
</button> | |
<button class="tab-button px-4 py-2 font-medium text-sm text-gray-500 focus:outline-none" data-tab="custom"> | |
<i class="fas fa-sliders-h mr-2"></i> Custom Settings | |
</button> | |
</div> | |
<!-- Presets Section --> | |
<div id="presets-section" class="transform-section active"> | |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6"> | |
<!-- Preset Cards --> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="chat"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-comments text-indigo-500"></i> | |
</div> | |
<h3 class="font-medium">Chat Format</h3> | |
</div> | |
<p class="text-sm text-gray-600">Converts to {"messages": [{"role": "user", "content": "..."}]}</p> | |
<div class="mt-3 text-xs text-indigo-600"> | |
<i class="fas fa-check-circle mr-1"></i> Best for conversational AI | |
</div> | |
</div> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="instruction"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-graduation-cap text-green-500"></i> | |
</div> | |
<h3 class="font-medium">Instruction Format</h3> | |
</div> | |
<p class="text-sm text-gray-600">Converts to {"instruction": "...", "response": "..."}</p> | |
<div class="mt-3 text-xs text-green-600"> | |
<i class="fas fa-check-circle mr-1"></i> Best for instruction-following models | |
</div> | |
</div> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="qa"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-question-circle text-blue-500"></i> | |
</div> | |
<h3 class="font-medium">Q&A Format</h3> | |
</div> | |
<p class="text-sm text-gray-600">Converts to {"question": "...", "answer": "..."}</p> | |
<div class="mt-3 text-xs text-blue-600"> | |
<i class="fas fa-check-circle mr-1"></i> Best for question answering | |
</div> | |
</div> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="completion"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-align-left text-purple-500"></i> | |
</div> | |
<h3 class="font-medium">Completion Format</h3> | |
</div> | |
<p class="text-sm text-gray-600">Converts to {"prompt": "...", "completion": "..."}</p> | |
<div class="mt-3 text-xs text-purple-600"> | |
<i class="fas fa-check-circle mr-1"></i> Best for text completion | |
</div> | |
</div> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="rename"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-yellow-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-tags text-yellow-500"></i> | |
</div> | |
<h3 class="font-medium">Rename Columns</h3> | |
</div> | |
<p class="text-sm text-gray-600">Lets you rename columns without changing structure</p> | |
<div class="mt-3 text-xs text-yellow-600"> | |
<i class="fas fa-check-circle mr-1"></i> Basic transformation | |
</div> | |
</div> | |
<div class="preset-card p-4 border rounded-lg cursor-pointer bg-white" data-preset="custom"> | |
<div class="flex items-center mb-2"> | |
<div class="w-10 h-10 rounded-full bg-pink-100 flex items-center justify-center mr-3"> | |
<i class="fas fa-code text-pink-500"></i> | |
</div> | |
<h3 class="font-medium">Custom Template</h3> | |
</div> | |
<p class="text-sm text-gray-600">Define your own structure with placeholders</p> | |
<div class="mt-3 text-xs text-pink-600"> | |
<i class="fas fa-check-circle mr-1"></i> Advanced users | |
</div> | |
</div> | |
</div> | |
<!-- Preset Configuration --> | |
<div id="preset-config" class="hidden mb-6"> | |
<h3 class="text-lg font-medium text-gray-800 mb-3" id="preset-title">Configure Preset</h3> | |
<div id="chat-config" class="preset-config-section hidden"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">User Message Column</label> | |
<select id="chat-user-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Assistant Message Column</label> | |
<select id="chat-assistant-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">System Message (optional)</label> | |
<input type="text" id="chat-system-msg" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500" placeholder="You are a helpful assistant..."> | |
</div> | |
</div> | |
<div id="instruction-config" class="preset-config-section hidden"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Instruction Column</label> | |
<select id="instruction-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Response Column</label> | |
<select id="response-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
</div> | |
</div> | |
<div id="qa-config" class="preset-config-section hidden"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Question Column</label> | |
<select id="question-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Answer Column</label> | |
<select id="answer-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
</div> | |
</div> | |
<div id="completion-config" class="preset-config-section hidden"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Prompt Column</label> | |
<select id="prompt-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Completion Column</label> | |
<select id="completion-col" class="w-full px-3 py-2 border rounded-md focus:ring-indigo-500 focus:border-indigo-500"> | |
<!-- Dynamically populated --> | |
</select> | |
</div> | |
</div> | |
</div> | |
<div id="rename-config" class="preset-config-section hidden"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Column Renaming</label> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<div class="grid grid-cols-2 gap-4 mb-4"> | |
<div class="text-sm font-medium text-gray-500">Original Name</div> | |
<div class="text-sm font-medium text-gray-500">New Name</div> | |
</div> | |
<div id="rename-mapping-container"> | |
<!-- Dynamic rename fields will be inserted here --> | |
</div> | |
</div> | |
</div> | |
<div id="custom-config" class="preset-config-section hidden"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Custom Template</label> | |
<textarea id="custom-template" class="w-full h-32 px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Example: { 'prompt': '{{input}}', 'completion': '{{output}}' }"></textarea> | |
<p class="text-xs text-gray-500 mt-1">Use {{column_name}} to reference columns from your data</p> | |
</div> | |
</div> | |
</div> | |
<!-- Custom Settings Section --> | |
<div id="custom-section" class="transform-section"> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
<!-- Format Selection --> | |
<div class="col-span-1"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Output Format</label> | |
<div class="space-y-2"> | |
<div class="p-4 border rounded-lg bg-white"> | |
<div class="flex items-center"> | |
<input type="radio" name="format" id="format-json" value="json" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500" checked> | |
<label for="format-json" class="ml-3 block text-sm font-medium text-gray-700">JSON</label> | |
</div> | |
<p class="mt-1 ml-7 text-xs text-gray-500">Structured data with key-value pairs</p> | |
</div> | |
<div class="p-4 border rounded-lg bg-white"> | |
<div class="flex items-center"> | |
<input type="radio" name="format" id="format-csv" value="csv" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500"> | |
<label for="format-csv" class="ml-3 block text-sm font-medium text-gray-700">CSV</label> | |
</div> | |
<p class="mt-1 ml-7 text-xs text-gray-500">Comma-separated values</p> | |
</div> | |
</div> | |
</div> | |
<!-- Header Mapping --> | |
<div class="col-span-2"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Header Mapping</label> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<div class="grid grid-cols-2 gap-4 mb-4"> | |
<div class="text-sm font-medium text-gray-500">Original Header</div> | |
<div class="text-sm font-medium text-gray-500">New Header Name</div> | |
</div> | |
<div id="header-mapping-container"> | |
<!-- Dynamic header mapping fields will be inserted here --> | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-table fa-2x mb-2"></i> | |
<p>Select a preset or upload a file to see available headers</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Preview Section --> | |
<div class="mb-6"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="block text-sm font-medium text-gray-700">Preview</label> | |
<button id="refresh-preview" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center"> | |
<i class="fas fa-sync-alt mr-1"></i> Refresh | |
</button> | |
</div> | |
<div id="preview-content" class="preview-container bg-gray-50 p-4 rounded-lg border border-gray-200 text-sm font-mono"> | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-eye fa-2x mb-2"></i> | |
<p>Select a preset or configure settings to see a preview</p> | |
</div> | |
</div> | |
</div> | |
<div class="flex justify-between items-center"> | |
<button id="prev-step-2" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"> | |
<i class="fas fa-arrow-left mr-2"></i> Back | |
</button> | |
<button id="next-step-2" class="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"> | |
Next <i class="fas fa-arrow-right ml-2"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Step 3: Download Result --> | |
<div id="step3" class="p-8 hidden"> | |
<div class="mb-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-3">Download Converted Data</h2> | |
<p class="text-gray-600">Your dataset has been successfully transformed</p> | |
</div> | |
<div class="bg-indigo-50 border border-indigo-100 rounded-lg p-6 text-center mb-8"> | |
<div class="mx-auto w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4"> | |
<i class="fas fa-check-circle text-indigo-500 text-2xl"></i> | |
</div> | |
<h3 class="text-lg font-medium text-gray-700 mb-2">Conversion Complete!</h3> | |
<p class="text-gray-600 mb-4">Your data is ready for fine-tuning</p> | |
<div class="flex justify-center space-x-4"> | |
<button id="download-btn" class="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
<i class="fas fa-download mr-2"></i> Download | |
</button> | |
<button id="copy-clipboard" class="px-6 py-2 bg-white text-indigo-600 border border-indigo-300 rounded-md hover:bg-indigo-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
<i class="fas fa-copy mr-2"></i> Copy to Clipboard | |
</button> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Converted Data Preview</label> | |
<div id="final-preview" class="preview-container bg-gray-50 p-4 rounded-lg border border-gray-200 text-sm font-mono"> | |
<!-- Final preview will be shown here --> | |
</div> | |
</div> | |
<div class="flex justify-between items-center"> | |
<button id="prev-step-3" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"> | |
<i class="fas fa-arrow-left mr-2"></i> Back | |
</button> | |
<button id="new-conversion" class="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
<i class="fas fa-redo mr-2"></i> Start New Conversion | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Footer --> | |
<footer class="mt-12 text-center text-gray-500 text-sm"> | |
<p>Dataset Converter Tool | Built for AI Fine-tuning | <a href="#" class="text-indigo-600 hover:underline">Privacy Policy</a></p> | |
</footer> | |
</div> | |
<script> | |
// Global variables | |
let uploadedFile = null; | |
let parsedData = null; | |
let headers = []; | |
let convertedData = null; | |
let selectedPreset = null; | |
// DOM elements | |
const dropzone = document.getElementById('dropzone'); | |
const fileUpload = document.getElementById('file-upload'); | |
const nextStep1Btn = document.getElementById('next-step-1'); | |
const step1 = document.getElementById('step1'); | |
const step2 = document.getElementById('step2'); | |
const step3 = document.getElementById('step3'); | |
const headerMappingContainer = document.getElementById('header-mapping-container'); | |
const previewContent = document.getElementById('preview-content'); | |
const finalPreview = document.getElementById('final-preview'); | |
const downloadBtn = document.getElementById('download-btn'); | |
const copyClipboardBtn = document.getElementById('copy-clipboard'); | |
const uploadIcon = document.getElementById('upload-icon'); | |
const dropzoneTitle = document.getElementById('dropzone-title'); | |
const dropzoneSubtitle = document.getElementById('dropzone-subtitle'); | |
const progressContainer = document.getElementById('progress-container'); | |
const progressBarFill = document.getElementById('progress-bar-fill'); | |
const progressText = document.getElementById('progress-text'); | |
const fileInfo = document.getElementById('file-info'); | |
const fileName = document.getElementById('file-name'); | |
const fileMeta = document.getElementById('file-meta'); | |
const fileIcon = document.getElementById('file-icon'); | |
const removeFile = document.getElementById('remove-file'); | |
const errorMessage = document.getElementById('error-message'); | |
const presetCards = document.querySelectorAll('.preset-card'); | |
const presetConfig = document.getElementById('preset-config'); | |
const presetTitle = document.getElementById('preset-title'); | |
const tabButtons = document.querySelectorAll('.tab-button'); | |
const presetsSection = document.getElementById('presets-section'); | |
const customSection = document.getElementById('custom-section'); | |
// Event listeners for navigation | |
document.getElementById('next-step-1').addEventListener('click', () => { | |
step1.classList.add('hidden'); | |
step2.classList.remove('hidden'); | |
updateStepNavigation(2); | |
}); | |
document.getElementById('prev-step-2').addEventListener('click', () => { | |
step2.classList.add('hidden'); | |
step1.classList.remove('hidden'); | |
updateStepNavigation(1); | |
}); | |
document.getElementById('next-step-2').addEventListener('click', () => { | |
convertData(); | |
step2.classList.add('hidden'); | |
step3.classList.remove('hidden'); | |
updateStepNavigation(3); | |
displayFinalPreview(); | |
}); | |
document.getElementById('prev-step-3').addEventListener('click', () => { | |
step3.classList.add('hidden'); | |
step2.classList.remove('hidden'); | |
updateStepNavigation(2); | |
}); | |
document.getElementById('new-conversion').addEventListener('click', () => { | |
resetConverter(); | |
step3.classList.add('hidden'); | |
step1.classList.remove('hidden'); | |
updateStepNavigation(1); | |
}); | |
// Tab switching | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
const tab = button.dataset.tab; | |
// Update tab buttons | |
tabButtons.forEach(btn => { | |
btn.classList.remove('active', 'text-indigo-600'); | |
btn.classList.add('text-gray-500'); | |
}); | |
button.classList.add('active', 'text-indigo-600'); | |
button.classList.remove('text-gray-500'); | |
// Show corresponding section | |
if (tab === 'presets') { | |
presetsSection.classList.add('active'); | |
customSection.classList.remove('active'); | |
} else { | |
presetsSection.classList.remove('active'); | |
customSection.classList.add('active'); | |
} | |
}); | |
}); | |
// Preset selection | |
presetCards.forEach(card => { | |
card.addEventListener('click', () => { | |
// Remove selection from all cards | |
presetCards.forEach(c => { | |
c.classList.remove('selected'); | |
c.classList.remove('border-indigo-500'); | |
c.classList.add('border-gray-200'); | |
}); | |
// Select clicked card | |
card.classList.add('selected', 'border-indigo-500'); | |
card.classList.remove('border-gray-200'); | |
// Show configuration for selected preset | |
selectedPreset = card.dataset.preset; | |
presetConfig.classList.remove('hidden'); | |
presetTitle.textContent = `Configure ${card.querySelector('h3').textContent}`; | |
// Hide all config sections | |
document.querySelectorAll('.preset-config-section').forEach(section => { | |
section.classList.add('hidden'); | |
}); | |
// Show selected config section | |
document.getElementById(`${selectedPreset}-config`).classList.remove('hidden'); | |
// Populate dropdowns if needed | |
if (uploadedFile) { | |
populatePresetDropdowns(); | |
} | |
// Update preview | |
updatePreview(); | |
}); | |
}); | |
// Refresh preview button | |
document.getElementById('refresh-preview').addEventListener('click', updatePreview); | |
// Download button | |
downloadBtn.addEventListener('click', downloadConvertedData); | |
// Copy to clipboard | |
copyClipboardBtn.addEventListener('click', copyToClipboard); | |
// Remove file button | |
removeFile.addEventListener('click', resetFileUpload); | |
// Dropzone events | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, preventDefaults, false); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
['dragenter', 'dragover'].forEach(eventName => { | |
dropzone.addEventListener(eventName, highlight, false); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, unhighlight, false); | |
}); | |
function highlight() { | |
dropzone.classList.add('active'); | |
} | |
function unhighlight() { | |
dropzone.classList.remove('active'); | |
} | |
dropzone.addEventListener('drop', handleDrop, false); | |
fileUpload.addEventListener('change', handleFileSelect, false); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
if (files.length > 1) { | |
showError('Please upload only one file at a time'); | |
return; | |
} | |
handleFiles(files); | |
} | |
function handleFileSelect(e) { | |
if (e.target.files.length > 1) { | |
showError('Please upload only one file at a time'); | |
return; | |
} | |
handleFiles(e.target.files); | |
} | |
function handleFiles(files) { | |
if (files.length > 0) { | |
const file = files[0]; | |
// Check file size (max 10MB) | |
if (file.size > 10 * 1024 * 1024) { | |
showError('File size exceeds 10MB limit'); | |
return; | |
} | |
// Check file type | |
const fileType = file.name.split('.').pop().toLowerCase(); | |
if (!['csv', 'json', 'xlsx', 'xls', 'tsv'].includes(fileType)) { | |
showError('Unsupported file format. Please upload CSV, JSON, Excel, or TSV files.'); | |
return; | |
} | |
// Show loading state | |
showUploadingState(); | |
// Simulate upload progress (in a real app, this would be actual upload progress) | |
let progress = 0; | |
const progressInterval = setInterval(() => { | |
progress += 5; | |
if (progress > 95) { | |
clearInterval(progressInterval); | |
parseFile(file); | |
} else { | |
updateProgress(progress); | |
} | |
}, 50); | |
} | |
} | |
function showUploadingState() { | |
// Hide error if any | |
hideError(); | |
// Show progress bar | |
progressContainer.classList.remove('hidden'); | |
progressBarFill.style.width = '0%'; | |
progressText.textContent = 'Uploading...'; | |
// Change icon to spinner | |
uploadIcon.innerHTML = '<i class="fas fa-spinner spinner text-indigo-500 text-2xl"></i>'; | |
dropzoneTitle.textContent = 'Uploading your file...'; | |
dropzoneSubtitle.classList.add('hidden'); | |
} | |
function updateProgress(percent) { | |
progressBarFill.style.width = `${percent}%`; | |
progressText.textContent = `Uploading... ${percent}%`; | |
} | |
function showFileInfo(file) { | |
const fileType = file.name.split('.').pop().toLowerCase(); | |
const fileSize = formatFileSize(file.size); | |
// Set file icon based on type | |
let iconClass = 'fa-file-alt'; | |
if (fileType === 'csv' || fileType === 'tsv') { | |
iconClass = 'fa-file-csv'; | |
} else if (fileType === 'json') { | |
iconClass = 'fa-file-code'; | |
} else if (fileType === 'xlsx' || fileType === 'xls') { | |
iconClass = 'fa-file-excel'; | |
} | |
fileIcon.className = `fas ${iconClass} text-indigo-500 mr-2`; | |
// Set file name and metadata | |
fileName.textContent = file.name; | |
fileMeta.textContent = `${fileSize} • ${fileType.toUpperCase()} file`; | |
// Show file info and hide progress | |
fileInfo.style.display = 'block'; | |
progressContainer.classList.add('hidden'); | |
// Update dropzone appearance | |
dropzone.classList.add('success'); | |
dropzone.classList.remove('active', 'error'); | |
// Update upload icon and text | |
uploadIcon.innerHTML = '<i class="fas fa-check-circle text-green-500 text-2xl"></i>'; | |
dropzoneTitle.textContent = 'File uploaded successfully'; | |
dropzoneSubtitle.classList.add('hidden'); | |
} | |
function formatFileSize(bytes) { | |
if (bytes < 1024) return bytes + ' bytes'; | |
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; | |
else return (bytes / 1048576).toFixed(1) + ' MB'; | |
} | |
function showError(message) { | |
errorMessage.textContent = message; | |
errorMessage.classList.remove('hidden'); | |
// Update dropzone appearance | |
dropzone.classList.add('error'); | |
dropzone.classList.remove('active', 'success'); | |
// Reset upload icon and text | |
uploadIcon.innerHTML = '<i class="fas fa-cloud-upload-alt text-indigo-500 text-2xl"></i>'; | |
dropzoneTitle.textContent = 'Drag & drop your file here'; | |
dropzoneSubtitle.classList.remove('hidden'); | |
// Hide progress and file info | |
progressContainer.classList.add('hidden'); | |
fileInfo.style.display = 'none'; | |
} | |
function hideError() { | |
errorMessage.classList.add('hidden'); | |
dropzone.classList.remove('error'); | |
} | |
function resetFileUpload() { | |
fileUpload.value = ''; | |
uploadedFile = null; | |
parsedData = null; | |
headers = []; | |
nextStep1Btn.disabled = true; | |
// Reset dropzone appearance | |
dropzone.classList.remove('active', 'error', 'success'); | |
// Reset UI elements | |
uploadIcon.innerHTML = '<i class="fas fa-cloud-upload-alt text-indigo-500 text-2xl"></i>'; | |
dropzoneTitle.textContent = 'Drag & drop your file here'; | |
dropzoneSubtitle.classList.remove('hidden'); | |
progressContainer.classList.add('hidden'); | |
fileInfo.style.display = 'none'; | |
hideError(); | |
// Reset header mapping | |
headerMappingContainer.innerHTML = ` | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-table fa-2x mb-2"></i> | |
<p>Upload a file to see available headers</p> | |
</div> | |
`; | |
// Reset preview | |
previewContent.innerHTML = ` | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-eye fa-2x mb-2"></i> | |
<p>Configure settings to see a preview</p> | |
</div> | |
`; | |
// Reset preset selection | |
presetCards.forEach(card => { | |
card.classList.remove('selected', 'border-indigo-500'); | |
card.classList.add('border-gray-200'); | |
}); | |
presetConfig.classList.add('hidden'); | |
selectedPreset = null; | |
} | |
function parseFile(file) { | |
const reader = new FileReader(); | |
const fileType = file.name.split('.').pop().toLowerCase(); | |
reader.onloadstart = function() { | |
progressText.textContent = 'Processing file...'; | |
}; | |
reader.onload = function(e) { | |
try { | |
if (fileType === 'csv' || fileType === 'tsv') { | |
parseCSV(e.target.result, fileType === 'tsv' ? '\t' : ','); | |
} else if (fileType === 'json') { | |
parsedData = JSON.parse(e.target.result); | |
extractHeadersFromJSON(); | |
} else if (fileType === 'xlsx' || fileType === 'xls') { | |
// In a real implementation, you would use SheetJS or similar library | |
// For this demo, we'll just show an error | |
throw new Error('Excel parsing would require a library like SheetJS. In this demo, please use CSV or JSON.'); | |
} else { | |
throw new Error('Unsupported file format'); | |
} | |
// Update UI | |
uploadedFile = file; | |
showFileInfo(file); | |
nextStep1Btn.disabled = false; | |
updateHeaderMapping(); | |
// If a preset is already selected, populate its dropdowns | |
if (selectedPreset) { | |
populatePresetDropdowns(); | |
} | |
updatePreview(); | |
// Complete progress | |
progressBarFill.style.width = '100%'; | |
progressText.textContent = 'Processing complete!'; | |
setTimeout(() => progressContainer.classList.add('hidden'), 1000); | |
} catch (error) { | |
console.error('Error parsing file:', error); | |
showError(`Error processing file: ${error.message}`); | |
progressContainer.classList.add('hidden'); | |
} | |
}; | |
reader.onerror = function() { | |
showError('Error reading file. Please try again.'); | |
progressContainer.classList.add('hidden'); | |
}; | |
if (fileType === 'csv' || fileType === 'tsv' || fileType === 'json') { | |
reader.readAsText(file); | |
} else { | |
reader.readAsBinaryString(file); | |
} | |
} | |
function parseCSV(data, delimiter) { | |
const lines = data.split('\n'); | |
if (lines.length === 0) { | |
throw new Error('File is empty'); | |
} | |
headers = lines[0].split(delimiter).map(h => h.trim()); | |
if (headers.length === 0) { | |
throw new Error('No headers found in the file'); | |
} | |
parsedData = []; | |
for (let i = 1; i < lines.length; i++) { | |
if (lines[i].trim() === '') continue; | |
const values = lines[i].split(delimiter); | |
const row = {}; | |
for (let j = 0; j < headers.length; j++) { | |
if (j < values.length) { | |
row[headers[j]] = values[j].trim(); | |
} else { | |
row[headers[j]] = ''; | |
} | |
} | |
parsedData.push(row); | |
} | |
if (parsedData.length === 0) { | |
throw new Error('No data rows found in the file'); | |
} | |
} | |
function extractHeadersFromJSON() { | |
if (Array.isArray(parsedData) && parsedData.length > 0) { | |
headers = Object.keys(parsedData[0]); | |
} else if (typeof parsedData === 'object') { | |
headers = Object.keys(parsedData); | |
// Convert to array format for consistency | |
parsedData = [parsedData]; | |
} else { | |
throw new Error('Invalid JSON structure. Expected an array of objects or a single object.'); | |
} | |
if (headers.length === 0) { | |
throw new Error('No headers found in the JSON data'); | |
} | |
} | |
function updateHeaderMapping() { | |
headerMappingContainer.innerHTML = ''; | |
if (headers.length === 0) { | |
headerMappingContainer.innerHTML = ` | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-exclamation-circle fa-2x mb-2"></i> | |
<p>No headers found in the uploaded file</p> | |
</div> | |
`; | |
return; | |
} | |
headers.forEach(header => { | |
const div = document.createElement('div'); | |
div.className = 'grid grid-cols-2 gap-4 mb-3 items-center'; | |
div.innerHTML = ` | |
<div class="text-sm text-gray-700 truncate" title="${header}">${header}</div> | |
<input type="text" value="${header}" class="header-input w-full px-3 py-2 text-gray-700 border rounded-md focus:border-indigo-500 smooth-transition" data-original="${header}"> | |
`; | |
headerMappingContainer.appendChild(div); | |
}); | |
} | |
function populatePresetDropdowns() { | |
if (!uploadedFile || !selectedPreset) return; | |
// Get all select elements in the current preset config | |
const selects = document.querySelectorAll(`#${selectedPreset}-config select`); | |
selects.forEach(select => { | |
// Clear existing options | |
select.innerHTML = ''; | |
// Add default option | |
const defaultOption = document.createElement('option'); | |
defaultOption.value = ''; | |
defaultOption.textContent = 'Select a column...'; | |
select.appendChild(defaultOption); | |
// Add options for each header | |
headers.forEach(header => { | |
const option = document.createElement('option'); | |
option.value = header; | |
option.textContent = header; | |
select.appendChild(option); | |
}); | |
}); | |
// For rename preset, populate the rename fields | |
if (selectedPreset === 'rename') { | |
const container = document.getElementById('rename-mapping-container'); | |
container.innerHTML = ''; | |
headers.forEach(header => { | |
const div = document.createElement('div'); | |
div.className = 'grid grid-cols-2 gap-4 mb-3 items-center'; | |
div.innerHTML = ` | |
<div class="text-sm text-gray-700 truncate" title="${header}">${header}</div> | |
<input type="text" value="${header}" class="rename-input w-full px-3 py-2 text-gray-700 border rounded-md focus:border-indigo-500 smooth-transition" data-original="${header}"> | |
`; | |
container.appendChild(div); | |
}); | |
} | |
} | |
function updatePreview() { | |
if (!uploadedFile) return; | |
// If using presets tab | |
if (presetsSection.classList.contains('active') && selectedPreset) { | |
let previewText = ''; | |
if (selectedPreset === 'chat') { | |
const userCol = document.getElementById('chat-user-col').value; | |
const assistantCol = document.getElementById('chat-assistant-col').value; | |
const systemMsg = document.getElementById('chat-system-msg').value; | |
if (userCol && assistantCol) { | |
const sample = getSampleData(1)[0]; | |
const messages = [ | |
systemMsg ? {role: "system", content: systemMsg} : null, | |
{role: "user", content: sample[userCol]}, | |
{role: "assistant", content: sample[assistantCol]} | |
].filter(Boolean); | |
previewText = JSON.stringify({messages}, null, 2); | |
} else { | |
previewText = 'Select user and assistant message columns to see preview'; | |
} | |
} | |
else if (selectedPreset === 'instruction') { | |
const instructionCol = document.getElementById('instruction-col').value; | |
const responseCol = document.getElementById('response-col').value; | |
if (instructionCol && responseCol) { | |
const sample = getSampleData(1)[0]; | |
previewText = JSON.stringify({ | |
instruction: sample[instructionCol], | |
response: sample[responseCol] | |
}, null, 2); | |
} else { | |
previewText = 'Select instruction and response columns to see preview'; | |
} | |
} | |
else if (selectedPreset === 'qa') { | |
const questionCol = document.getElementById('question-col').value; | |
const answerCol = document.getElementById('answer-col').value; | |
if (questionCol && answerCol) { | |
const sample = getSampleData(1)[0]; | |
previewText = JSON.stringify({ | |
question: sample[questionCol], | |
answer: sample[answerCol] | |
}, null, 2); | |
} else { | |
previewText = 'Select question and answer columns to see preview'; | |
} | |
} | |
else if (selectedPreset === 'completion') { | |
const promptCol = document.getElementById('prompt-col').value; | |
const completionCol = document.getElementById('completion-col').value; | |
if (promptCol && completionCol) { | |
const sample = getSampleData(1)[0]; | |
previewText = JSON.stringify({ | |
prompt: sample[promptCol], | |
completion: sample[completionCol] | |
}, null, 2); | |
} else { | |
previewText = 'Select prompt and completion columns to see preview'; | |
} | |
} | |
else if (selectedPreset === 'rename') { | |
const inputs = document.querySelectorAll('.rename-input'); | |
const mapping = {}; | |
inputs.forEach(input => { | |
mapping[input.dataset.original] = input.value || input.dataset.original; | |
}); | |
const sample = getSampleData(1)[0]; | |
const renamedSample = {}; | |
for (const oldHeader in mapping) { | |
renamedSample[mapping[oldHeader]] = sample[oldHeader]; | |
} | |
previewText = JSON.stringify(renamedSample, null, 2); | |
} | |
else if (selectedPreset === 'custom') { | |
const template = document.getElementById('custom-template').value.trim(); | |
if (template) { | |
try { | |
const sample = getSampleData(1)[0]; | |
let result = template; | |
// Replace placeholders with actual values | |
for (const key in sample) { | |
const placeholder = `{{${key}}}`; | |
if (template.includes(placeholder)) { | |
result = result.replace(new RegExp(placeholder, 'g'), sample[key]); | |
} | |
} | |
previewText = result; | |
} catch (e) { | |
previewText = 'Error applying template. Check your syntax.'; | |
} | |
} else { | |
previewText = 'Enter a custom template to see a preview'; | |
} | |
} | |
previewContent.innerHTML = `<pre class="text-gray-800">${escapeHtml(previewText)}</pre>`; | |
} | |
// If using custom settings tab | |
else { | |
const selectedFormat = document.querySelector('input[name="format"]:checked').value; | |
let previewText = ''; | |
if (selectedFormat === 'json') { | |
const sample = getSampleData(3); | |
previewText = JSON.stringify(sample, null, 2); | |
} else if (selectedFormat === 'csv') { | |
const newHeaders = getNewHeaders(); | |
previewText = newHeaders.join(',') + '\n'; | |
const sample = getSampleData(3); | |
sample.forEach(row => { | |
const values = newHeaders.map(h => row[h] || ''); | |
previewText += values.join(',') + '\n'; | |
}); | |
} | |
previewContent.innerHTML = `<pre class="text-gray-800">${escapeHtml(previewText)}</pre>`; | |
} | |
} | |
function getSampleData(count) { | |
return parsedData.slice(0, Math.min(count, parsedData.length)); | |
} | |
function getNewHeaders() { | |
const inputs = document.querySelectorAll('.header-input'); | |
const newHeaders = []; | |
inputs.forEach(input => { | |
newHeaders.push(input.value || input.dataset.original); | |
}); | |
return newHeaders; | |
} | |
function getHeaderMapping() { | |
const inputs = document.querySelectorAll('.header-input'); | |
const mapping = {}; | |
inputs.forEach(input => { | |
mapping[input.dataset.original] = input.value || input.dataset.original; | |
}); | |
return mapping; | |
} | |
function convertData() { | |
// If using presets tab | |
if (presetsSection.classList.contains('active') && selectedPreset) { | |
if (selectedPreset === 'chat') { | |
const userCol = document.getElementById('chat-user-col').value; | |
const assistantCol = document.getElementById('chat-assistant-col').value; | |
const systemMsg = document.getElementById('chat-system-msg').value; | |
if (userCol && assistantCol) { | |
convertedData = parsedData.map(row => { | |
const messages = [ | |
systemMsg ? {role: "system", content: systemMsg} : null, | |
{role: "user", content: row[userCol]}, | |
{role: "assistant", content: row[assistantCol]} | |
].filter(Boolean); | |
return {messages}; | |
}); | |
} | |
} | |
else if (selectedPreset === 'instruction') { | |
const instructionCol = document.getElementById('instruction-col').value; | |
const responseCol = document.getElementById('response-col').value; | |
if (instructionCol && responseCol) { | |
convertedData = parsedData.map(row => ({ | |
instruction: row[instructionCol], | |
response: row[responseCol] | |
})); | |
} | |
} | |
else if (selectedPreset === 'qa') { | |
const questionCol = document.getElementById('question-col').value; | |
const answerCol = document.getElementById('answer-col').value; | |
if (questionCol && answerCol) { | |
convertedData = parsedData.map(row => ({ | |
question: row[questionCol], | |
answer: row[answerCol] | |
})); | |
} | |
} | |
else if (selectedPreset === 'completion') { | |
const promptCol = document.getElementById('prompt-col').value; | |
const completionCol = document.getElementById('completion-col').value; | |
if (promptCol && completionCol) { | |
convertedData = parsedData.map(row => ({ | |
prompt: row[promptCol], | |
completion: row[completionCol] | |
})); | |
} | |
} | |
else if (selectedPreset === 'rename') { | |
const inputs = document.querySelectorAll('.rename-input'); | |
const mapping = {}; | |
inputs.forEach(input => { | |
mapping[input.dataset.original] = input.value || input.dataset.original; | |
}); | |
convertedData = parsedData.map(row => { | |
const newRow = {}; | |
for (const oldHeader in mapping) { | |
newRow[mapping[oldHeader]] = row[oldHeader]; | |
} | |
return newRow; | |
}); | |
} | |
else if (selectedPreset === 'custom') { | |
const template = document.getElementById('custom-template').value.trim(); | |
if (template) { | |
try { | |
convertedData = parsedData.map(row => { | |
let result = template; | |
for (const key in row) { | |
const placeholder = `{{${key}}}`; | |
if (template.includes(placeholder)) { | |
result = result.replace(new RegExp(placeholder, 'g'), row[key]); | |
} | |
} | |
return result; | |
}).join('\n'); | |
} catch (e) { | |
convertedData = 'Error in custom template: ' + e.message; | |
} | |
} else { | |
convertedData = 'No custom template provided'; | |
} | |
} | |
} | |
// If using custom settings tab | |
else { | |
const selectedFormat = document.querySelector('input[name="format"]:checked').value; | |
const headerMapping = getHeaderMapping(); | |
if (selectedFormat === 'json') { | |
// Simple renaming of headers | |
convertedData = parsedData.map(row => { | |
const newRow = {}; | |
for (const oldHeader in headerMapping) { | |
newRow[headerMapping[oldHeader]] = row[oldHeader]; | |
} | |
return newRow; | |
}); | |
} else if (selectedFormat === 'csv') { | |
const newHeaders = Object.values(headerMapping); | |
let csvContent = newHeaders.join(',') + '\n'; | |
parsedData.forEach(row => { | |
const values = newHeaders.map(h => { | |
// Find the original header that maps to this new header | |
const originalHeader = Object.keys(headerMapping).find(key => headerMapping[key] === h); | |
return row[originalHeader] || ''; | |
}); | |
csvContent += values.join(',') + '\n'; | |
}); | |
convertedData = csvContent; | |
} | |
} | |
} | |
function displayFinalPreview() { | |
if (!convertedData) return; | |
if (typeof convertedData === 'string') { | |
finalPreview.innerHTML = `<pre class="text-gray-800">${escapeHtml(convertedData)}</pre>`; | |
} else { | |
finalPreview.innerHTML = `<pre class="text-gray-800">${escapeHtml(JSON.stringify(convertedData, null, 2))}</pre>`; | |
} | |
} | |
function downloadConvertedData() { | |
if (!convertedData) return; | |
let blob, filename; | |
if (typeof convertedData === 'string') { | |
blob = new Blob([convertedData], { type: 'text/csv' }); | |
filename = 'converted_data.csv'; | |
} else { | |
const dataStr = JSON.stringify(convertedData, null, 2); | |
blob = new Blob([dataStr], { type: 'application/json' }); | |
filename = 'converted_data.json'; | |
} | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); | |
} | |
function copyToClipboard() { | |
if (!convertedData) return; | |
let textToCopy; | |
if (typeof convertedData === 'string') { | |
textToCopy = convertedData; | |
} else { | |
textToCopy = JSON.stringify(convertedData, null, 2); | |
} | |
navigator.clipboard.writeText(textToCopy).then(() => { | |
const originalText = copyClipboardBtn.innerHTML; | |
copyClipboardBtn.innerHTML = '<i class="fas fa-check mr-2"></i> Copied!'; | |
setTimeout(() => { | |
copyClipboardBtn.innerHTML = originalText; | |
}, 2000); | |
}).catch(err => { | |
console.error('Failed to copy: ', err); | |
alert('Failed to copy to clipboard'); | |
}); | |
} | |
function resetConverter() { | |
uploadedFile = null; | |
parsedData = null; | |
headers = []; | |
convertedData = null; | |
selectedPreset = null; | |
fileUpload.value = ''; | |
nextStep1Btn.disabled = true; | |
headerMappingContainer.innerHTML = ` | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-table fa-2x mb-2"></i> | |
<p>Upload a file to see available headers</p> | |
</div> | |
`; | |
previewContent.innerHTML = ` | |
<div class="text-center py-8 text-gray-400"> | |
<i class="fas fa-eye fa-2x mb-2"></i> | |
<p>Configure settings to see a preview</p> | |
</div> | |
`; | |
finalPreview.innerHTML = ''; | |
// Reset dropzone appearance | |
dropzone.classList.remove('active', 'error', 'success'); | |
uploadIcon.innerHTML = '<i class="fas fa-cloud-upload-alt text-indigo-500 text-2xl"></i>'; | |
dropzoneTitle.textContent = 'Drag & drop your file here'; | |
dropzoneSubtitle.classList.remove('hidden'); | |
progressContainer.classList.add('hidden'); | |
fileInfo.style.display = 'none'; | |
hideError(); | |
// Reset preset selection | |
presetCards.forEach(card => { | |
card.classList.remove('selected', 'border-indigo-500'); | |
card.classList.add('border-gray-200'); | |
}); | |
presetConfig.classList.add('hidden'); | |
// Reset tabs to presets | |
tabButtons.forEach((btn, index) => { | |
if (index === 0) { | |
btn.classList.add('active', 'text-indigo-600'); | |
btn.classList.remove('text-gray-500'); | |
} else { | |
btn.classList.remove('active', 'text-indigo-600'); | |
btn.classList.add('text-gray-500'); | |
} | |
}); | |
presetsSection.classList.add('active'); | |
customSection.classList.remove('active'); | |
} | |
function updateStepNavigation(step) { | |
const steps = document.querySelectorAll('.flex.border-b.border-gray-200 > div'); | |
steps.forEach((stepEl, index) => { | |
if (index + 1 === step) { | |
stepEl.classList.remove('border-gray-200', 'text-gray-500'); | |
stepEl.classList.add('border-indigo-500', 'text-indigo-600'); | |
} else { | |
stepEl.classList.remove('border-indigo-500', 'text-indigo-600'); | |
stepEl.classList.add('border-gray-200', 'text-gray-500'); | |
} | |
}); | |
} | |
function escapeHtml(unsafe) { | |
return unsafe | |
.replace(/&/g, "&") | |
.replace(/</g, "<") | |
.replace(/>/g, ">") | |
.replace(/"/g, """) | |
.replace(/'/g, "'"); | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=CultriX/deepsite" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |