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