File size: 36,351 Bytes
6c3cead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61d85e4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONL Dataset Maker</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Google Fonts Inter -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Inter', sans-serif;
        }
        .code-font {
            font-family: 'Fira Code', monospace;
        }
    </style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col selection:bg-indigo-500 selection:text-white">

    <!-- Header -->
    <header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50 px-6 py-4">
        <div class="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
            <div class="flex items-center gap-3">
                <div class="h-10 w-10 rounded-xl bg-gradient-to-tr from-indigo-500 to-violet-500 flex items-center justify-center shadow-lg shadow-indigo-500/20">
                    <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
                    </svg>
                </div>
                <div>
                    <h1 class="text-xl font-bold tracking-tight bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">JSONL Dataset Architect</h1>
                    <p class="text-xs text-slate-400">Build high-quality multi-turn or single-turn AI training data</p>
                </div>
            </div>
            
            <div class="flex items-center gap-3">
                <span class="text-xs bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-3 py-1.5 rounded-full font-medium">
                    Active Dataset Model
                </span>
            </div>
        </div>
    </header>

    <!-- Main Content Grid -->
    <main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 grid grid-cols-1 lg:grid-cols-12 gap-6">
        
        <!-- Left Column: Controls and Input Form (7 cols) -->
        <section class="lg:col-span-7 flex flex-col gap-6">
            
            <!-- Format Selector Card -->
            <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl">
                <label class="block text-sm font-semibold text-slate-300 mb-3">Choose Dataset Format</label>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
                    <div class="relative">
                        <select id="formatSelector" onchange="handleFormatChange()" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all cursor-pointer appearance-none">
                            <option value="reasoning">Reasoning Messages (with think tags)</option>
                            <option value="messages">Messages (Normal, no reasoning)</option>
                            <option value="sharegpt">ShareGPT Conversations</option>
                            <option value="alpaca">Alpaca (Single Turn)</option>
                            <option value="custom">Custom JSON Structure</option>
                        </select>
                        <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400">
                            <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
                        </div>
                    </div>
                    <div id="formatBadge" class="flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center">
                        Multi-turn conversations supported
                    </div>
                </div>
            </div>

            <!-- Current Entry Editor Form -->
            <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex-1 flex flex-col gap-5">
                <div class="flex justify-between items-center pb-4 border-b border-slate-800">
                    <h2 class="text-md font-bold text-slate-200 flex items-center gap-2">
                        <svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
                        </svg>
                        Construct Current Line Entry
                    </h2>
                    <span id="turnsCount" class="text-xs bg-slate-800 px-2.5 py-1 rounded-md text-slate-400 font-medium">
                        0 Turns Created
                    </span>
                </div>

                <!-- Input Fields Container -->
                <div id="dynamicInputsContainer" class="flex-1 space-y-4 max-h-[50vh] overflow-y-auto pr-1">
                    <!-- Loaded dynamically via JavaScript based on Format Selector -->
                </div>

                <!-- Add Turn controls (hidden for Alpaca format) -->
                <div id="turnControls" class="flex flex-wrap gap-2 pt-2 border-t border-slate-800/50">
                    <button type="button" onclick="addTurn('user')" class="flex-1 min-w-[120px] bg-slate-800 hover:bg-slate-750 text-slate-200 hover:text-white px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all border border-slate-700/50">
                        <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m0-6H6" />
                        </svg>
                        Add User Turn
                    </button>
                    <button type="button" onclick="addTurn('assistant')" class="flex-1 min-w-[120px] bg-slate-800 hover:bg-slate-750 text-slate-200 hover:text-white px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all border border-slate-700/50">
                        <svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m0-6H6" />
                        </svg>
                        Add Assistant Turn
                    </button>
                </div>

                <!-- Submission Controls -->
                <div class="flex gap-3 pt-3">
                    <button type="button" onclick="resetCurrentForm()" class="px-5 py-3 rounded-xl text-xs font-bold bg-slate-950 hover:bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 transition-all">
                        Reset Editor
                    </button>
                    <button type="button" onclick="commitEntry()" class="flex-1 bg-gradient-to-r from-indigo-500 to-violet-500 hover:from-indigo-600 hover:to-violet-600 text-white px-6 py-3 rounded-xl text-xs font-bold shadow-lg shadow-indigo-500/15 hover:shadow-indigo-500/25 transition-all flex items-center justify-center gap-2">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
                        </svg>
                        Add Line to JSONL
                    </button>
                </div>
            </div>
        </section>

        <!-- Right Column: Live Preview & Line Manager (5 cols) -->
        <section class="lg:col-span-5 flex flex-col gap-6">
            
            <!-- Live Preview -->
            <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex flex-col h-[400px]">
                <div class="flex justify-between items-center pb-4 border-b border-slate-800">
                    <div class="flex items-center gap-2">
                        <div class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></div>
                        <h2 class="text-md font-bold text-slate-200">Live Preview</h2>
                    </div>
                    <button onclick="copyToClipboard()" class="bg-slate-800 hover:bg-indigo-600 hover:text-white text-slate-300 px-3 py-1.5 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all border border-slate-700/50">
                        <svg id="copyIcon" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
                        </svg>
                        <span id="copyText">Copy Output</span>
                    </button>
                </div>

                <!-- JSONL Preview Output -->
                <div class="flex-1 overflow-auto mt-4 bg-slate-950 border border-slate-850 rounded-xl p-4 relative">
                    <pre id="jsonlPreview" class="code-font text-xs text-indigo-300 whitespace-pre-wrap break-all select-all leading-relaxed h-full">{}</pre>
                    <div id="emptyState" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-950 p-6 text-center">
                        <svg class="w-12 h-12 text-slate-700 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
                        </svg>
                        <span class="text-sm font-semibold text-slate-500">No JSON Lines Created</span>
                        <span class="text-xs text-slate-600 mt-1">Populate input fields and add a line to start building your dataset.</span>
                    </div>
                </div>
            </div>

            <!-- List Management -->
            <div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl flex-1 flex flex-col min-h-[250px]">
                <div class="flex justify-between items-center pb-4 border-b border-slate-800">
                    <h2 class="text-sm font-bold text-slate-200">Lines in Dataset</h2>
                    <button onclick="clearDataset()" class="text-rose-400 hover:text-rose-300 text-xs font-semibold transition-all">
                        Clear All Lines
                    </button>
                </div>

                <!-- Lines List Area -->
                <div id="datasetLinesContainer" class="flex-1 overflow-y-auto space-y-2 mt-4 max-h-[250px] pr-1">
                    <!-- Items mapped via JS -->
                </div>
            </div>
        </section>
    </main>

    <!-- Custom Modal/Toast Notifications -->
    <div id="toast" class="fixed bottom-6 right-6 z-50 bg-slate-900 border border-slate-800 px-4 py-3 rounded-xl shadow-2xl flex items-center gap-3 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none">
        <div id="toastIcon" class="h-6 w-6 rounded-full flex items-center justify-center"></div>
        <p id="toastMessage" class="text-xs font-medium text-slate-200"></p>
    </div>

    <!-- Hidden input helper for secure clipboard manipulation -->
    <textarea id="clipboardHelper" class="sr-only" aria-hidden="true"></textarea>

    <!-- App Logic Script -->
    <script>
        // Configuration options
        const APP_STATE = {
            currentFormat: 'reasoning',
            dataset: [],
            currentTurns: [],
            // Custom format structures
            customKeys: ['prompt', 'response']
        };

        // DOM elements
        const formatSelector = document.getElementById('formatSelector');
        const formatBadge = document.getElementById('formatBadge');
        const turnsCount = document.getElementById('turnsCount');
        const dynamicInputsContainer = document.getElementById('dynamicInputsContainer');
        const turnControls = document.getElementById('turnControls');
        const jsonlPreview = document.getElementById('jsonlPreview');
        const emptyState = document.getElementById('emptyState');
        const datasetLinesContainer = document.getElementById('datasetLinesContainer');
        const clipboardHelper = document.getElementById('clipboardHelper');
        const toastElement = document.getElementById('toast');
        const toastMsg = document.getElementById('toastMessage');
        const toastIcon = document.getElementById('toastIcon');

        // Setup onload
        window.onload = function() {
            handleFormatChange();
        };

        function handleFormatChange() {
            APP_STATE.currentFormat = formatSelector.value;
            resetCurrentForm();
            
            // Adjust metadata badge and controls based on selection
            if (APP_STATE.currentFormat === 'alpaca') {
                formatBadge.innerText = "Single-turn instruction data format";
                formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center";
                turnControls.classList.add('hidden');
                turnsCount.classList.add('hidden');
                buildAlpacaInputs();
            } else if (APP_STATE.currentFormat === 'custom') {
                formatBadge.innerText = "Custom single-turn Key-Value mapping";
                formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center";
                turnControls.classList.add('hidden');
                turnsCount.classList.add('hidden');
                buildCustomInputs();
            } else {
                formatBadge.innerText = "Multi-turn conversations supported";
                formatBadge.className = "flex items-center justify-center px-4 py-3 bg-indigo-950/30 text-indigo-400 border border-indigo-900/50 rounded-xl text-xs font-semibold text-center";
                turnControls.classList.remove('hidden');
                turnsCount.classList.remove('hidden');
                
                // Add first turn default structure
                addTurn('system');
                addTurn('user');
                addTurn('assistant');
            }
        }

        function addTurn(role) {
            const turnId = 'turn_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
            const turnObj = { id: turnId, role: role, content: '' };
            
            if (role === 'assistant' && APP_STATE.currentFormat === 'reasoning') {
                turnObj.reasoning = '';
            }

            APP_STATE.currentTurns.push(turnObj);
            renderInputs();
            updateTurnsBadge();
        }

        function removeTurn(id) {
            APP_STATE.currentTurns = APP_STATE.currentTurns.filter(turn => turn.id !== id);
            renderInputs();
            updateTurnsBadge();
        }

        function updateTurnContent(id, field, value) {
            const index = APP_STATE.currentTurns.findIndex(turn => turn.id === id);
            if (index !== -1) {
                APP_STATE.currentTurns[index][field] = value;
            }
        }

        function updateTurnsBadge() {
            const userAndAssistantTurns = APP_STATE.currentTurns.filter(t => t.role !== 'system').length;
            turnsCount.innerText = `${userAndAssistantTurns} Turn${userAndAssistantTurns !== 1 ? 's' : ''} Created`;
        }

        function buildAlpacaInputs() {
            dynamicInputsContainer.innerHTML = `
                <div class="space-y-4">
                    <div>
                        <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Instruction</label>
                        <textarea id="alp_instruction" placeholder="What should the AI do?" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[90px] transition-all resize-y"></textarea>
                    </div>
                    <div>
                        <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Input (Optional context)</label>
                        <textarea id="alp_input" placeholder="e.g. context text, variables, or background data" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[80px] transition-all resize-y"></textarea>
                    </div>
                    <div>
                        <label class="block text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wide">Output</label>
                        <textarea id="alp_output" placeholder="The golden response output" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[100px] transition-all resize-y"></textarea>
                    </div>
                </div>
            `;
        }

        function buildCustomInputs() {
            let fieldsHtml = '';
            APP_STATE.customKeys.forEach((key, index) => {
                fieldsHtml += `
                    <div class="flex items-center gap-3 bg-slate-950/40 p-3 rounded-xl border border-slate-850">
                        <div class="flex-1">
                            <input type="text" value="${key}" placeholder="Key name" onchange="updateCustomKeyName(${index}, this.value)" class="code-font bg-slate-950 border border-slate-800 rounded-lg px-2 py-1 text-xs text-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 mb-1.5 font-semibold">
                            <textarea id="custom_val_${index}" placeholder="Value content..." class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 min-h-[60px] resize-y"></textarea>
                        </div>
                        <button type="button" onclick="removeCustomField(${index})" class="text-rose-500 hover:text-rose-400 hover:bg-slate-900 p-2 rounded-lg transition-all" title="Remove dynamic key">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                            </svg>
                        </button>
                    </div>
                `;
            });

            dynamicInputsContainer.innerHTML = `
                <div class="space-y-4">
                    <div class="flex justify-between items-center">
                        <label class="block text-xs font-bold text-slate-400 uppercase tracking-wide">Mapped Custom Keys</label>
                        <button type="button" onclick="addCustomField()" class="text-indigo-400 hover:text-indigo-300 text-xs font-semibold flex items-center gap-1">
                            + Add Key Field
                        </button>
                    </div>
                    <div class="space-y-3">
                        ${fieldsHtml}
                    </div>
                </div>
            `;
        }

        function addCustomField() {
            const nextIndex = APP_STATE.customKeys.length + 1;
            APP_STATE.customKeys.push(`key_${nextIndex}`);
            buildCustomInputs();
        }

        function removeCustomField(index) {
            APP_STATE.customKeys.splice(index, 1);
            buildCustomInputs();
        }

        function updateCustomKeyName(index, newName) {
            const standardizedName = newName.trim().replace(/[^a-zA-Z0-9_]/g, '');
            APP_STATE.customKeys[index] = standardizedName || `key_${index + 1}`;
        }

        function renderInputs() {
            if (APP_STATE.currentFormat === 'alpaca' || APP_STATE.currentFormat === 'custom') {
                return; // Managed individually
            }

            dynamicInputsContainer.innerHTML = '';
            
            APP_STATE.currentTurns.forEach((turn, index) => {
                const isSystem = turn.role === 'system';
                const isUser = turn.role === 'user' || turn.role === 'human';
                
                // Set style accents based on role
                let roleLabel = turn.role.toUpperCase();
                let borderTheme = 'border-slate-800';
                let tagTheme = 'bg-slate-950 border border-slate-850 text-slate-400';
                let inputPlaceholder = 'Enter system prompt instructions...';

                if (isUser) {
                    borderTheme = 'border-emerald-950/40 bg-emerald-950/5';
                    tagTheme = 'bg-emerald-950/40 text-emerald-400 border border-emerald-900/50';
                    inputPlaceholder = 'What would the human/user ask the model?';
                } else if (turn.role === 'assistant' || turn.role === 'gpt') {
                    borderTheme = 'border-indigo-950/40 bg-indigo-950/5';
                    tagTheme = 'bg-indigo-950/40 text-indigo-400 border border-indigo-900/50';
                    inputPlaceholder = 'Model response goes here...';
                }

                // Compile reasoning fields html inside reasoning format assistant inputs
                let reasoningFieldHtml = '';
                if (turn.role === 'assistant' && APP_STATE.currentFormat === 'reasoning') {
                    reasoningFieldHtml = `
                        <div class="mt-2.5">
                            <label class="block text-[10px] font-semibold text-slate-500 mb-1">Reasoning Content (Wrapped inside think tags)</label>
                            <textarea placeholder="Write the model reasoning trajectory..." oninput="updateTurnContent('${turn.id}', 'reasoning', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-indigo-300 placeholder-indigo-950 focus:outline-none focus:ring-1 focus:ring-indigo-500 min-h-[60px] resize-y">${turn.reasoning || ''}</textarea>
                        </div>
                    `;
                }

                const turnDiv = document.createElement('div');
                turnDiv.className = `p-4 rounded-xl border ${borderTheme} flex flex-col gap-2 relative group transition-all`;
                turnDiv.innerHTML = `
                    <div class="flex justify-between items-center">
                        <span class="text-[10px] font-bold px-2.5 py-1 rounded-md ${tagTheme} tracking-wide uppercase">
                            ${roleLabel}
                        </span>
                        
                        <button type="button" onclick="removeTurn('${turn.id}')" class="text-slate-600 hover:text-rose-400 hover:bg-slate-900 p-1 rounded-lg transition-all" title="Delete turn">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                            </svg>
                        </button>
                    </div>

                    ${reasoningFieldHtml}

                    <div class="mt-1">
                        <textarea placeholder="${inputPlaceholder}" oninput="updateTurnContent('${turn.id}', 'content', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 min-h-[70px] resize-y">${turn.content || ''}</textarea>
                    </div>
                `;

                dynamicInputsContainer.appendChild(turnDiv);
            });
        }

        function commitEntry() {
            let newEntry = null;

            if (APP_STATE.currentFormat === 'alpaca') {
                const instruction = document.getElementById('alp_instruction').value.trim();
                const input = document.getElementById('alp_input').value.trim();
                const output = document.getElementById('alp_output').value.trim();

                if (!instruction || !output) {
                    showToast('error', 'Instruction and Output are required fields');
                    return;
                }

                newEntry = {
                    instruction: instruction,
                    input: input,
                    output: output
                };
            } else if (APP_STATE.currentFormat === 'custom') {
                newEntry = {};
                let validationError = false;

                APP_STATE.customKeys.forEach((key, index) => {
                    const textVal = document.getElementById(`custom_val_${index}`).value;
                    newEntry[key] = textVal;
                    if (!key.trim()) validationError = true;
                });

                if (validationError || APP_STATE.customKeys.length === 0) {
                    showToast('error', 'Please configure proper key values first');
                    return;
                }
            } else {
                // Handling conversational formats: Messages, Reasoning, ShareGPT
                const list = [];
                let hasFilledTurn = false;

                APP_STATE.currentTurns.forEach(turn => {
                    if (APP_STATE.currentFormat === 'sharegpt') {
                        // Transform role for ShareGPT
                        let sRole = 'human';
                        if (turn.role === 'system') sRole = 'system';
                        else if (turn.role === 'assistant') sRole = 'gpt';

                        list.push({
                            from: sRole,
                            value: turn.content
                        });
                        if (turn.content.trim()) hasFilledTurn = true;
                    } else {
                        // Messages & Reasoning
                        let messageContent = turn.content;
                        
                        // If reasoning model format and role is assistant, prep prepended think tag
                        if (APP_STATE.currentFormat === 'reasoning' && turn.role === 'assistant') {
                            const rawReasoning = turn.reasoning || '';
                            messageContent = `<think>${rawReasoning}</think>\n${turn.content}`;
                        }

                        list.push({
                            role: turn.role,
                            content: messageContent
                        });
                        if (turn.content.trim()) hasFilledTurn = true;
                    }
                });

                if (!hasFilledTurn) {
                    showToast('error', 'Cannot add empty turn array into dataset');
                    return;
                }

                if (APP_STATE.currentFormat === 'sharegpt') {
                    newEntry = { conversations: list };
                } else {
                    newEntry = { messages: list };
                }
            }

            // Append to array state
            APP_STATE.dataset.push(newEntry);
            updatePreview();
            renderDatasetLinesList();
            showToast('success', 'Entry added to dataset successfully');
            
            // Keep system templates on dynamic inputs but flush current contents for fresh loop
            resetCurrentContentKeepStructure();
        }

        function resetCurrentContentKeepStructure() {
            if (APP_STATE.currentFormat === 'alpaca') {
                document.getElementById('alp_instruction').value = '';
                document.getElementById('alp_input').value = '';
                document.getElementById('alp_output').value = '';
            } else if (APP_STATE.currentFormat === 'custom') {
                APP_STATE.customKeys.forEach((_, index) => {
                    const textarea = document.getElementById(`custom_val_${index}`);
                    if (textarea) textarea.value = '';
                });
            } else {
                APP_STATE.currentTurns.forEach(turn => {
                    turn.content = '';
                    if (turn.reasoning !== undefined) {
                        turn.reasoning = '';
                    }
                });
                renderInputs();
            }
        }

        function resetCurrentForm() {
            APP_STATE.currentTurns = [];
            if (APP_STATE.currentFormat === 'alpaca') {
                buildAlpacaInputs();
            } else if (APP_STATE.currentFormat === 'custom') {
                buildCustomInputs();
            } else {
                addTurn('system');
                addTurn('user');
                addTurn('assistant');
            }
        }

        function updatePreview() {
            if (APP_STATE.dataset.length === 0) {
                jsonlPreview.innerText = '{}';
                emptyState.classList.remove('hidden');
                return;
            }

            emptyState.classList.add('hidden');
            const lines = APP_STATE.dataset.map(item => JSON.stringify(item));
            jsonlPreview.innerText = lines.join('\n');
        }

        function renderDatasetLinesList() {
            datasetLinesContainer.innerHTML = '';
            
            APP_STATE.dataset.forEach((entry, idx) => {
                let entryHeader = `Line #${idx + 1}`;
                let entryDetails = '';

                if (entry.messages) {
                    const turnsCount = entry.messages.filter(m => m.role !== 'system').length;
                    entryDetails = `${turnsCount} multi-turn message interaction(s)`;
                } else if (entry.conversations) {
                    const turnsCount = entry.conversations.filter(m => m.from !== 'system').length;
                    entryDetails = `${turnsCount} conversation exchange(s)`;
                } else if (entry.instruction) {
                    entryDetails = 'Alpaca single-turn structured entry';
                } else {
                    entryDetails = 'Custom flat key-value dynamic format';
                }

                const entryDiv = document.createElement('div');
                entryDiv.className = "flex justify-between items-center bg-slate-950 p-3 rounded-xl border border-slate-850 hover:border-indigo-900/50 transition-all";
                entryDiv.innerHTML = `
                    <div class="truncate pr-4">
                        <p class="text-xs font-bold text-slate-300">${entryHeader}</p>
                        <p class="text-[10px] text-slate-500 truncate">${entryDetails}</p>
                    </div>
                    <button onclick="removeDatasetLine(${idx})" class="text-rose-500 hover:text-rose-400 hover:bg-slate-900 p-1.5 rounded-lg transition-all" title="Delete dataset line">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                        </svg>
                    </button>
                `;

                datasetLinesContainer.appendChild(entryDiv);
            });
        }

        function removeDatasetLine(index) {
            APP_STATE.dataset.splice(index, 1);
            updatePreview();
            renderDatasetLinesList();
            showToast('success', 'Dataset line removed');
        }

        function clearDataset() {
            if (APP_STATE.dataset.length === 0) return;
            APP_STATE.dataset = [];
            updatePreview();
            renderDatasetLinesList();
            showToast('success', 'Dataset cleared completely');
        }

        function copyToClipboard() {
            if (APP_STATE.dataset.length === 0) {
                showToast('error', 'Dataset is empty. Add entries first.');
                return;
            }

            const rawText = jsonlPreview.innerText;
            clipboardHelper.value = rawText;
            clipboardHelper.select();
            clipboardHelper.setSelectionRange(0, 99999); // Mobile compatibility

            try {
                document.execCommand('copy');
                
                // Show copied UI indicator temporary swap
                const copyText = document.getElementById('copyText');
                const copyIcon = document.getElementById('copyIcon');
                
                copyText.innerText = 'Copied!';
                copyIcon.innerHTML = `
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
                `;

                setTimeout(() => {
                    copyText.innerText = 'Copy Output';
                    copyIcon.innerHTML = `
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
                    `;
                }, 1500);

                showToast('success', 'JSONL output copied to clipboard!');
            } catch (err) {
                showToast('error', 'Failed to copy text safely.');
            }
        }

        function showToast(type, message) {
            // Apply theme config for toast icon
            if (type === 'success') {
                toastIcon.className = "h-6 w-6 rounded-full flex items-center justify-center bg-emerald-950 border border-emerald-900 text-emerald-400";
                toastIcon.innerHTML = `
                    <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
                    </svg>
                `;
            } else {
                toastIcon.className = "h-6 w-6 rounded-full flex items-center justify-center bg-rose-950 border border-rose-900 text-rose-400";
                toastIcon.innerHTML = `
                    <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                `;
            }

            toastMsg.innerText = message;
            
            // Pop Animation
            toastElement.classList.remove('opacity-0', 'translate-y-20', 'pointer-events-none');
            
            setTimeout(() => {
                toastElement.classList.add('opacity-0', 'translate-y-20', 'pointer-events-none');
            }, 3000);
        }
    </script>
</body>
</html>