wop commited on
Commit
138290d
·
verified ·
1 Parent(s): 23c13d4

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1536 -1005
index.html CHANGED
@@ -1,1059 +1,1590 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Self-Distillation</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- :root {
15
- --bg-primary: #0a0a0f;
16
- --bg-secondary: #12121a;
17
- --bg-tertiary: #1a1a25;
18
- --text-primary: #e0e0e0;
19
- --text-secondary: #888;
20
- --accent: #6366f1;
21
- --border: #2a2a3a;
22
- }
23
-
24
- body {
25
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
26
- background: var(--bg-primary);
27
- color: var(--text-primary);
28
- height: 100vh;
29
- overflow: hidden;
30
- }
31
-
32
- .container {
33
- display: flex;
34
- flex-direction: column;
35
- height: 100vh;
36
- padding: 20px;
37
- }
38
-
39
- /* Header */
40
- .header {
41
- display: flex;
42
- justify-content: space-between;
43
- align-items: center;
44
- margin-bottom: 20px;
45
- padding: 15px;
46
- background: var(--bg-secondary);
47
- border-radius: 12px;
48
- border: 1px solid var(--border);
49
- }
50
-
51
- .header h1 {
52
- font-size: 1.5rem;
53
- font-weight: 600;
54
- background: linear-gradient(135deg, #6366f1, #8b5cf6);
55
- -webkit-background-clip: text;
56
- -webkit-text-fill-color: transparent;
57
- }
58
-
59
- .header-controls {
60
- display: flex;
61
- gap: 10px;
62
- }
63
-
64
- .btn {
65
- padding: 8px 16px;
66
- border: none;
67
- border-radius: 6px;
68
- cursor: pointer;
69
- font-size: 0.9rem;
70
- transition: all 0.2s;
71
- }
72
-
73
- .btn-primary {
74
- background: var(--accent);
75
- color: white;
76
- }
77
-
78
- .btn-primary:hover {
79
- background: #5558e3;
80
- transform: translateY(-1px);
81
- }
82
-
83
- .btn-secondary {
84
- background: var(--bg-tertiary);
85
- color: var(--text-primary);
86
- border: 1px solid var(--border);
87
- }
88
-
89
- .btn-secondary:hover {
90
- background: #252535;
91
- }
92
-
93
- /* Editor Section */
94
- .editor-section {
95
- flex: 1;
96
- display: flex;
97
- flex-direction: column;
98
- margin-bottom: 20px;
99
- position: relative;
100
- }
101
-
102
- .editor-container {
103
- flex: 1;
104
- position: relative;
105
- border-radius: 12px;
106
- overflow: hidden;
107
- transition: all 0.3s ease;
108
- }
109
-
110
- .editor-glow {
111
- position: absolute;
112
- top: 0;
113
- left: 0;
114
- right: 0;
115
- bottom: 0;
116
- border-radius: 12px;
117
- pointer-events: none;
118
- opacity: 0;
119
- transition: opacity 0.3s;
120
- z-index: 1;
121
- }
122
-
123
- #editor {
124
- width: 100%;
125
- height: 100%;
126
- background: var(--bg-secondary);
127
- color: var(--text-primary);
128
- border: 1px solid var(--border);
129
- border-radius: 12px;
130
- padding: 20px;
131
- font-size: 1.1rem;
132
- line-height: 1.6;
133
- resize: none;
134
- outline: none;
135
- position: relative;
136
- z-index: 2;
137
- }
138
-
139
- #editor:focus {
140
- border-color: var(--accent);
141
- }
142
-
143
- /* Timeline Section */
144
- .timeline-section {
145
- background: var(--bg-secondary);
146
- border-radius: 12px;
147
- border: 1px solid var(--border);
148
- padding: 20px;
149
- min-height: 300px;
150
- }
151
-
152
- .timeline-header {
153
- display: flex;
154
- justify-content: space-between;
155
- align-items: center;
156
- margin-bottom: 15px;
157
- }
158
-
159
- .timeline-title {
160
- font-size: 1.1rem;
161
- font-weight: 600;
162
- }
163
-
164
- .emotion-input-container {
165
- display: flex;
166
- gap: 10px;
167
- align-items: center;
168
- }
169
-
170
- #emotionInput {
171
- padding: 8px 12px;
172
- background: var(--bg-tertiary);
173
- border: 1px solid var(--border);
174
- border-radius: 6px;
175
- color: var(--text-primary);
176
- font-size: 0.9rem;
177
- width: 200px;
178
- }
179
-
180
- #emotionInput::placeholder {
181
- color: var(--text-secondary);
182
- }
183
-
184
- /* Timeline Canvas */
185
- .timeline-canvas-container {
186
- position: relative;
187
- background: var(--bg-tertiary);
188
- border-radius: 8px;
189
- overflow: hidden;
190
- cursor: crosshair;
191
- }
192
-
193
- #timelineCanvas {
194
- display: block;
195
- width: 100%;
196
- }
197
-
198
- /* Instructions */
199
- .instructions {
200
- margin-top: 15px;
201
- padding: 12px;
202
- background: var(--bg-tertiary);
203
- border-radius: 6px;
204
- font-size: 0.85rem;
205
- color: var(--text-secondary);
206
- line-height: 1.5;
207
- }
208
-
209
- .instructions strong {
210
- color: var(--text-primary);
211
- }
212
-
213
- /* Modal */
214
- .modal {
215
- display: none;
216
- position: fixed;
217
- top: 0;
218
- left: 0;
219
- right: 0;
220
- bottom: 0;
221
- background: rgba(0, 0, 0, 0.8);
222
- z-index: 1000;
223
- justify-content: center;
224
- align-items: center;
225
- }
226
-
227
- .modal.active {
228
- display: flex;
229
- }
230
-
231
- .modal-content {
232
- background: var(--bg-secondary);
233
- border-radius: 12px;
234
- padding: 30px;
235
- max-width: 500px;
236
- width: 90%;
237
- border: 1px solid var(--border);
238
- }
239
-
240
- .modal-content h2 {
241
- margin-bottom: 20px;
242
- font-size: 1.3rem;
243
- }
244
-
245
- .form-group {
246
- margin-bottom: 15px;
247
- }
248
-
249
- .form-group label {
250
- display: block;
251
- margin-bottom: 5px;
252
- font-size: 0.9rem;
253
- color: var(--text-secondary);
254
- }
255
-
256
- .form-group input {
257
- width: 100%;
258
- padding: 10px;
259
- background: var(--bg-tertiary);
260
- border: 1px solid var(--border);
261
- border-radius: 6px;
262
- color: var(--text-primary);
263
- font-size: 0.9rem;
264
- }
265
-
266
- .modal-actions {
267
- display: flex;
268
- gap: 10px;
269
- justify-content: flex-end;
270
- margin-top: 20px;
271
- }
272
-
273
- /* Status */
274
- .status-bar {
275
- margin-top: 10px;
276
- padding: 10px;
277
- background: var(--bg-tertiary);
278
- border-radius: 6px;
279
- font-size: 0.85rem;
280
- color: var(--text-secondary);
281
- display: none;
282
- }
283
-
284
- .status-bar.active {
285
- display: block;
286
- }
287
-
288
- .status-bar.success {
289
- border-left: 3px solid #10b981;
290
- }
291
-
292
- .status-bar.error {
293
- border-left: 3px solid #ef4444;
294
- }
295
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  </head>
297
  <body>
298
- <div class="container">
299
- <!-- Header -->
300
- <div class="header">
301
- <h1>Self-Distillation</h1>
302
- <div class="header-controls">
303
- <button class="btn btn-secondary" onclick="exportJSON()">Export JSON</button>
304
- <button class="btn btn-secondary" onclick="showHFModal()">Push to HF</button>
305
- </div>
306
- </div>
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- <!-- Editor Section -->
309
- <div class="editor-section">
310
- <div class="editor-container">
311
- <div class="editor-glow" id="editorGlow"></div>
312
- <textarea id="editor" placeholder="Write your thoughts here..."></textarea>
313
- </div>
314
  </div>
 
 
 
 
 
 
 
 
 
 
315
 
316
- <!-- Timeline Section -->
317
- <div class="timeline-section">
318
- <div class="timeline-header">
319
- <div class="timeline-title">Emotional Timeline</div>
320
- <div class="emotion-input-container">
321
- <input type="text" id="emotionInput" placeholder="Enter emotion (e.g., Joy, Anxiety)">
322
- <button class="btn btn-primary" onclick="addEmotionTrack()">Add Track</button>
323
- </div>
324
- </div>
325
-
326
- <div class="timeline-canvas-container">
327
- <canvas id="timelineCanvas"></canvas>
328
- </div>
329
-
330
- <div class="instructions">
331
- <strong>Controls:</strong> Click on timeline to add point • Drag vertically for intensity (0-1) •
332
- Drag horizontally for timing • Drag handles for fades/bumps • Move cursor in text to see active emotions
333
- </div>
334
-
335
- <div class="status-bar" id="statusBar"></div>
336
  </div>
 
 
337
  </div>
338
 
339
- <!-- HuggingFace Modal -->
340
- <div class="modal" id="hfModal">
341
- <div class="modal-content">
342
- <h2>Push to HuggingFace</h2>
343
- <div class="form-group">
344
- <label>HF Token</label>
345
- <input type="password" id="hfToken" placeholder="hf_...">
346
- </div>
347
- <div class="form-group">
348
- <label>Repository ID</label>
349
- <input type="text" id="hfRepoId" placeholder="username/repo-name">
350
- </div>
351
- <div class="modal-actions">
352
- <button class="btn btn-secondary" onclick="closeHFModal()">Cancel</button>
353
- <button class="btn btn-primary" onclick="pushToHF()">Push Dataset</button>
354
- </div>
 
 
 
 
355
  </div>
 
 
 
 
 
356
  </div>
357
-
358
- <script>
359
- // State Management
360
- const state = {
361
- text: '',
362
- emotions: [],
363
- tracks: [],
364
- selectedPoint: null,
365
- dragging: false,
366
- dragType: null,
367
- dragStart: { x: 0, y: 0 },
368
- cursorPosition: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  };
370
-
371
- // DOM Elements
372
- const editor = document.getElementById('editor');
373
- const canvas = document.getElementById('timelineCanvas');
374
- const ctx = canvas.getContext('2d');
375
- const emotionInput = document.getElementById('emotionInput');
376
- const editorGlow = document.getElementById('editorGlow');
377
- const statusBar = document.getElementById('statusBar');
378
-
379
- // Color palette for emotions
380
- const colorPalette = [
381
- '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e',
382
- '#f97316', '#eab308', '#22c55e', '#06b6d4',
383
- '#3b82f6', '#a855f7'
384
- ];
385
-
386
- let colorIndex = 0;
387
-
388
- // Initialize
389
- function init() {
390
- loadFromLocalStorage();
391
- setupEventListeners();
392
- resizeCanvas();
393
- render();
394
- }
395
-
396
- // Event Listeners
397
- function setupEventListeners() {
398
- editor.addEventListener('input', handleTextChange);
399
- editor.addEventListener('click', handleEditorClick);
400
- editor.addEventListener('keyup', handleEditorClick);
401
-
402
- canvas.addEventListener('mousedown', handleCanvasMouseDown);
403
- canvas.addEventListener('mousemove', handleCanvasMouseMove);
404
- canvas.addEventListener('mouseup', handleCanvasMouseUp);
405
- canvas.addEventListener('mouseleave', handleCanvasMouseUp);
406
-
407
- window.addEventListener('resize', () => {
408
- resizeCanvas();
409
- render();
410
- });
411
- }
412
-
413
- // Text Handling
414
- function handleTextChange(e) {
415
- state.text = e.target.value;
416
- updateTimelineScale();
417
- render();
418
- saveToLocalStorage();
419
- }
420
-
421
- function handleEditorClick() {
422
- state.cursorPosition = editor.selectionStart;
423
- updateEditorGlow();
424
- render();
425
- }
426
-
427
- // Canvas Setup
428
- function resizeCanvas() {
429
- const container = canvas.parentElement;
430
- canvas.width = container.clientWidth;
431
- canvas.height = 300;
432
- }
433
-
434
- function updateTimelineScale() {
435
- // Adjust timeline based on text length
436
- if (state.text.length === 0) return;
437
- }
438
-
439
- // Emotion Management
440
- function addEmotionTrack() {
441
- const emotionName = emotionInput.value.trim();
442
- if (!emotionName) {
443
- showStatus('Please enter an emotion name', 'error');
444
- return;
445
- }
446
-
447
- const color = colorPalette[colorIndex % colorPalette.length];
448
- colorIndex++;
449
-
450
- const track = {
451
- id: Date.now(),
452
- label: emotionName,
453
- color: color,
454
- points: []
455
- };
456
-
457
- state.tracks.push(track);
458
- emotionInput.value = '';
459
- render();
460
- saveToLocalStorage();
461
- showStatus(`Added track: ${emotionName}`, 'success');
462
- }
463
-
464
- // Canvas Interaction
465
- function getCanvasCoordinates(e) {
466
- const rect = canvas.getBoundingClientRect();
467
- return {
468
- x: e.clientX - rect.left,
469
- y: e.clientY - rect.top
470
- };
471
- }
472
-
473
- function charIndexToX(charIndex) {
474
- const textLength = state.text.length || 1;
475
- const padding = 20;
476
- const availableWidth = canvas.width - (padding * 2);
477
- return padding + (charIndex / textLength) * availableWidth;
478
- }
479
-
480
- function xToCharIndex(x) {
481
- const padding = 20;
482
- const availableWidth = canvas.width - (padding * 2);
483
- const textLength = state.text.length || 1;
484
- const normalizedX = Math.max(0, Math.min(x - padding, availableWidth));
485
- return Math.round((normalizedX / availableWidth) * textLength);
486
- }
487
-
488
- function intensityToY(intensity) {
489
- const padding = 20;
490
- const availableHeight = canvas.height - (padding * 2);
491
- return padding + availableHeight - (intensity * availableHeight);
492
- }
493
-
494
- function yToIntensity(y) {
495
- const padding = 20;
496
- const availableHeight = canvas.height - (padding * 2);
497
- const normalizedY = Math.max(padding, Math.min(y, canvas.height - padding));
498
- return 1 - ((normalizedY - padding) / availableHeight);
499
- }
500
-
501
- function handleCanvasMouseDown(e) {
502
- const coords = getCanvasCoordinates(e);
503
- const charIndex = xToCharIndex(coords.x);
504
-
505
- // Check if clicking on existing point or handle
506
- const clickedItem = findItemAtPosition(coords.x, coords.y);
507
-
508
- if (clickedItem) {
509
- state.selectedPoint = clickedItem;
510
- state.dragging = true;
511
- state.dragType = clickedItem.type;
512
- state.dragStart = { x: coords.x, y: coords.y };
513
  } else {
514
- // Add new point to active track
515
- const activeTrack = state.tracks[state.tracks.length - 1];
516
- if (activeTrack) {
517
- const intensity = yToIntensity(coords.y);
518
- const newPoint = {
519
- id: Date.now(),
520
- charIndex: charIndex,
521
- intensity: Math.max(0, Math.min(1, intensity)),
522
- fadeIn: 0,
523
- fadeOut: 0
524
- };
525
- activeTrack.points.push(newPoint);
526
- activeTrack.points.sort((a, b) => a.charIndex - b.charIndex);
527
- state.selectedPoint = { type: 'point', point: newPoint, track: activeTrack };
528
- state.dragging = true;
529
- state.dragType = 'point';
530
- render();
531
- saveToLocalStorage();
532
- }
533
  }
534
- }
535
-
536
- function handleCanvasMouseMove(e) {
537
- if (!state.dragging || !state.selectedPoint) return;
538
-
539
- const coords = getCanvasCoordinates(e);
540
- const deltaX = coords.x - state.dragStart.x;
541
- const deltaY = coords.y - state.dragStart.y;
542
-
543
- if (state.dragType === 'point') {
544
- const point = state.selectedPoint.point;
545
-
546
- // Horizontal movement adjusts timing
547
- const newCharIndex = xToCharIndex(coords.x);
548
- point.charIndex = Math.max(0, Math.min(state.text.length, newCharIndex));
549
-
550
- // Vertical movement adjusts intensity
551
- const newIntensity = yToIntensity(coords.y);
552
- point.intensity = Math.max(0, Math.min(1, newIntensity));
553
-
554
- // Sort points after moving
555
- state.selectedPoint.track.points.sort((a, b) => a.charIndex - b.charIndex);
556
- } else if (state.dragType === 'fadeIn') {
557
- const point = state.selectedPoint.point;
558
- point.fadeIn = Math.max(0, Math.min(point.charIndex, point.fadeIn + deltaX / 10));
559
- } else if (state.dragType === 'fadeOut') {
560
- const point = state.selectedPoint.point;
561
- const maxFadeOut = state.text.length - point.charIndex;
562
- point.fadeOut = Math.max(0, Math.min(maxFadeOut, point.fadeOut + deltaX / 10));
563
- }
564
-
565
- state.dragStart = { x: coords.x, y: coords.y };
566
- render();
567
- saveToLocalStorage();
568
- }
569
-
570
- function handleCanvasMouseUp() {
571
- state.dragging = false;
572
- state.dragType = null;
573
- }
574
-
575
- function findItemAtPosition(x, y) {
576
- for (const track of state.tracks) {
577
- for (const point of track.points) {
578
- const pointX = charIndexToX(point.charIndex);
579
- const pointY = intensityToY(point.intensity);
580
-
581
- // Check main point
582
- if (Math.abs(x - pointX) < 10 && Math.abs(y - pointY) < 10) {
583
- return { type: 'point', point: point, track: track };
584
- }
585
-
586
- // Check fade-in handle
587
- if (point.fadeIn > 0) {
588
- const fadeInX = charIndexToX(point.charIndex - point.fadeIn);
589
- if (Math.abs(x - fadeInX) < 8 && Math.abs(y - pointY) < 8) {
590
- return { type: 'fadeIn', point: point, track: track };
591
- }
592
- }
593
-
594
- // Check fade-out handle
595
- if (point.fadeOut > 0) {
596
- const fadeOutX = charIndexToX(point.charIndex + point.fadeOut);
597
- if (Math.abs(x - fadeOutX) < 8 && Math.abs(y - pointY) < 8) {
598
- return { type: 'fadeOut', point: point, track: track };
599
- }
600
- }
601
- }
602
- }
603
- return null;
604
- }
605
-
606
- // Rendering
607
- function render() {
608
- ctx.clearRect(0, 0, canvas.width, canvas.height);
609
-
610
- // Draw grid
611
- drawGrid();
612
-
613
- // Draw tracks
614
- state.tracks.forEach(track => {
615
- drawTrack(track);
616
- });
617
-
618
- // Draw cursor indicator
619
- drawCursorIndicator();
620
- }
621
-
622
- function drawGrid() {
623
- ctx.strokeStyle = '#2a2a3a';
624
- ctx.lineWidth = 1;
625
-
626
- // Vertical lines (character markers)
627
- const step = Math.max(1, Math.floor(state.text.length / 20));
628
- for (let i = 0; i <= state.text.length; i += step) {
629
- const x = charIndexToX(i);
630
- ctx.beginPath();
631
- ctx.moveTo(x, 0);
632
- ctx.lineTo(x, canvas.height);
633
- ctx.stroke();
634
- }
635
-
636
- // Horizontal lines (intensity markers)
637
- for (let i = 0; i <= 1; i += 0.25) {
638
- const y = intensityToY(i);
639
- ctx.beginPath();
640
- ctx.moveTo(0, y);
641
- ctx.lineTo(canvas.width, y);
642
- ctx.stroke();
643
- }
644
- }
645
-
646
- function drawTrack(track) {
647
- if (track.points.length === 0) return;
648
-
649
- ctx.strokeStyle = track.color;
650
- ctx.fillStyle = track.color;
651
- ctx.lineWidth = 2;
652
-
653
- // Draw curve
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  ctx.beginPath();
655
- track.points.forEach((point, index) => {
656
- const x = charIndexToX(point.charIndex);
657
- const y = intensityToY(point.intensity);
658
-
659
- if (index === 0) {
660
- ctx.moveTo(x, y);
661
- } else {
662
- ctx.lineTo(x, y);
663
- }
664
- });
665
  ctx.stroke();
666
-
667
- // Draw fill under curve
668
- ctx.globalAlpha = 0.1;
669
- ctx.lineTo(charIndexToX(track.points[track.points.length - 1].charIndex), canvas.height - 20);
670
- ctx.lineTo(charIndexToX(track.points[0].charIndex), canvas.height - 20);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  ctx.closePath();
 
672
  ctx.fill();
673
- ctx.globalAlpha = 1;
674
-
675
- // Draw points and handles
676
- track.points.forEach(point => {
677
- drawPoint(point, track);
678
- });
679
- }
680
 
681
- function drawPoint(point, track) {
682
- const x = charIndexToX(point.charIndex);
683
- const y = intensityToY(point.intensity);
684
-
685
- // Main point
686
  ctx.beginPath();
687
- ctx.arc(x, y, 6, 0, Math.PI * 2);
688
- ctx.fillStyle = track.color;
689
- ctx.fill();
690
- ctx.strokeStyle = '#fff';
691
- ctx.lineWidth = 2;
692
- ctx.stroke();
693
-
694
- // Fade-in handle
695
- if (point.fadeIn > 0) {
696
- const fadeInX = charIndexToX(point.charIndex - point.fadeIn);
697
- ctx.beginPath();
698
- ctx.arc(fadeInX, y, 4, 0, Math.PI * 2);
699
- ctx.fillStyle = track.color;
700
- ctx.fill();
701
-
702
- ctx.beginPath();
703
- ctx.moveTo(fadeInX, y);
704
- ctx.lineTo(x, y);
705
- ctx.strokeStyle = track.color;
706
- ctx.lineWidth = 1;
707
- ctx.setLineDash([3, 3]);
708
- ctx.stroke();
709
- ctx.setLineDash([]);
710
- }
711
-
712
- // Fade-out handle
713
- if (point.fadeOut > 0) {
714
- const fadeOutX = charIndexToX(point.charIndex + point.fadeOut);
715
- ctx.beginPath();
716
- ctx.arc(fadeOutX, y, 4, 0, Math.PI * 2);
717
- ctx.fillStyle = track.color;
718
- ctx.fill();
719
-
720
- ctx.beginPath();
721
- ctx.moveTo(x, y);
722
- ctx.lineTo(fadeOutX, y);
723
- ctx.strokeStyle = track.color;
724
- ctx.lineWidth = 1;
725
- ctx.setLineDash([3, 3]);
726
- ctx.stroke();
727
- ctx.setLineDash([]);
728
  }
729
- }
730
-
731
- function drawCursorIndicator() {
732
- if (state.text.length === 0) return;
733
-
734
- const x = charIndexToX(state.cursorPosition);
735
-
736
- ctx.strokeStyle = '#fff';
737
  ctx.lineWidth = 2;
738
- ctx.setLineDash([5, 5]);
739
- ctx.beginPath();
740
- ctx.moveTo(x, 0);
741
- ctx.lineTo(x, canvas.height);
742
  ctx.stroke();
743
- ctx.setLineDash([]);
744
- }
745
 
746
- // Editor Glow Effect
747
- function updateEditorGlow() {
748
- const activeEmotions = getActiveEmotionsAtPosition(state.cursorPosition);
749
-
750
- if (activeEmotions.length === 0) {
751
- editorGlow.style.opacity = '0';
752
- return;
753
- }
754
-
755
- // Calculate weighted average color and intensity
756
- let totalWeight = 0;
757
- let r = 0, g = 0, b = 0;
758
-
759
- activeEmotions.forEach(emotion => {
760
- const color = hexToRgb(emotion.color);
761
- const weight = emotion.intensity;
762
- r += color.r * weight;
763
- g += color.g * weight;
764
- b += color.b * weight;
765
- totalWeight += weight;
766
- });
767
-
768
- if (totalWeight > 0) {
769
- r = Math.round(r / totalWeight);
770
- g = Math.round(g / totalWeight);
771
- b = Math.round(b / totalWeight);
772
-
773
- const avgIntensity = totalWeight / activeEmotions.length;
774
- editorGlow.style.background = `radial-gradient(circle at center, rgba(${r}, ${g}, ${b}, ${avgIntensity * 0.3}), transparent)`;
775
- editorGlow.style.opacity = '1';
776
  }
777
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
 
779
- function getActiveEmotionsAtPosition(charIndex) {
780
- const activeEmotions = [];
781
-
782
- state.tracks.forEach(track => {
783
- track.points.forEach(point => {
784
- const start = point.charIndex - point.fadeIn;
785
- const end = point.charIndex + point.fadeOut;
786
-
787
- if (charIndex >= start && charIndex <= end) {
788
- let intensity = point.intensity;
789
-
790
- // Apply fade-in
791
- if (charIndex < point.charIndex && point.fadeIn > 0) {
792
- const fadeProgress = (charIndex - start) / point.fadeIn;
793
- intensity *= fadeProgress;
794
- }
795
-
796
- // Apply fade-out
797
- if (charIndex > point.charIndex && point.fadeOut > 0) {
798
- const fadeProgress = 1 - ((charIndex - point.charIndex) / point.fadeOut);
799
- intensity *= fadeProgress;
800
- }
801
-
802
- activeEmotions.push({
803
- label: track.label,
804
- color: track.color,
805
- intensity: Math.max(0, Math.min(1, intensity))
806
- });
807
- }
808
- });
809
- });
810
-
811
- return activeEmotions;
812
- }
813
 
814
- // Data Export
815
- function generateDataset() {
816
- const emotionalFlow = [];
817
-
818
- state.tracks.forEach(track => {
819
- const curves = track.points.map(point => ({
820
- start_char: Math.max(0, point.charIndex - point.fadeIn),
821
- end_char: Math.min(state.text.length, point.charIndex + point.fadeOut),
822
- start_intensity: point.fadeIn > 0 ? 0 : point.intensity,
823
- peak_intensity: point.intensity,
824
- end_intensity: point.fadeOut > 0 ? 0 : point.intensity
825
- }));
826
-
827
- if (curves.length > 0) {
828
- emotionalFlow.push({
829
- label: track.label,
830
- curves: curves
831
- });
832
- }
833
- });
834
-
835
- return {
836
- text: state.text,
837
- emotional_flow: emotionalFlow
838
- };
839
- }
840
 
841
- function exportJSON() {
842
- const dataset = generateDataset();
843
- const jsonStr = JSON.stringify(dataset, null, 2);
844
-
845
- const blob = new Blob([jsonStr], { type: 'application/json' });
846
- const url = URL.createObjectURL(blob);
847
- const a = document.createElement('a');
848
- a.href = url;
849
- a.download = 'self-distillation-dataset.json';
850
- a.click();
851
- URL.revokeObjectURL(url);
852
-
853
- showStatus('Dataset exported successfully', 'success');
854
- }
855
 
856
- // HuggingFace Integration
857
- function showHFModal() {
858
- const savedToken = localStorage.getItem('hf_token');
859
- const savedRepo = localStorage.getItem('hf_repo_id');
860
-
861
- if (savedToken) document.getElementById('hfToken').value = savedToken;
862
- if (savedRepo) document.getElementById('hfRepoId').value = savedRepo;
863
-
864
- document.getElementById('hfModal').classList.add('active');
865
- }
866
 
867
- function closeHFModal() {
868
- document.getElementById('hfModal').classList.remove('active');
869
- }
870
 
871
- async function pushToHF() {
872
- const token = document.getElementById('hfToken').value.trim();
873
- const repoId = document.getElementById('hfRepoId').value.trim();
874
-
875
- if (!token || !repoId) {
876
- showStatus('Please provide both token and repository ID', 'error');
877
- return;
878
- }
879
-
880
- // Save credentials
881
- localStorage.setItem('hf_token', token);
882
- localStorage.setItem('hf_repo_id', repoId);
883
-
884
- try {
885
- showStatus('Preparing dataset...', 'success');
886
-
887
- const dataset = generateDataset();
888
-
889
- // Create Parquet-like binary data (simplified)
890
- const parquetData = convertToParquet(dataset);
891
-
892
- // Create dataset card (Jinja2 format)
893
- const datasetCard = generateDatasetCard(dataset);
894
-
895
- // Upload files
896
- await uploadToHF(token, repoId, parquetData, datasetCard);
897
-
898
- showStatus('Successfully pushed to HuggingFace!', 'success');
899
- closeHFModal();
900
- } catch (error) {
901
- showStatus(`Error: ${error.message}`, 'error');
902
- }
903
- }
904
 
905
- function convertToParquet(dataset) {
906
- // Simplified Parquet conversion
907
- // In production, use a proper Parquet library
908
- const records = [];
909
-
910
- dataset.emotional_flow.forEach(emotion => {
911
- emotion.curves.forEach(curve => {
912
- records.push({
913
- text: dataset.text,
914
- emotion_label: emotion.label,
915
- start_char: curve.start_char,
916
- end_char: curve.end_char,
917
- start_intensity: curve.start_intensity,
918
- peak_intensity: curve.peak_intensity,
919
- end_intensity: curve.end_intensity
920
- });
921
- });
922
- });
923
-
924
- // Return as JSON for simplicity (real implementation would use parquetjs)
925
- return JSON.stringify(records, null, 2);
926
- }
927
 
928
- function generateDatasetCard(dataset) {
929
- return `---
930
- language: en
931
- license: mit
932
- task_categories:
933
- - text-classification
934
- - sentiment-analysis
935
  tags:
936
- - emotion-detection
 
937
  - self-distillation
938
- - affective-computing
939
- pretty_name: Self-Distillation Dataset
940
  ---
941
 
942
- # Self-Distillation Dataset
943
 
944
- ## Dataset Description
945
- This dataset contains text annotated with emotional flow curves, mapping character-level positions to emotional intensities.
946
 
947
- ## Dataset Structure
948
- - **text**: The original text content
949
- - **emotional_flow**: Array of emotions with their intensity curves
950
- - **label**: Emotion name
951
- - **curves**: Array of intensity curves with start/end positions and intensities
952
 
953
- ## Usage
954
- \`\`\`python
955
- import json
956
 
957
- with open('dataset.json', 'r') as f:
958
- data = json.load(f)
959
-
960
- for entry in data['emotional_flow']:
961
- print(f"Emotion: {entry['label']}")
962
- for curve in entry['curves']:
963
- print(f" Position {curve['start_char']}-{curve['end_char']}: {curve['peak_intensity']}")
964
- \`\`\`
965
 
966
- ## Generation
967
- Generated using the Self-Distillation web app with manual annotation.
968
- `;
969
- }
 
 
 
 
970
 
971
- async function uploadToHF(token, repoId, parquetData, datasetCard) {
972
- const headers = {
973
- 'Authorization': `Bearer ${token}`,
974
- 'Content-Type': 'application/json'
975
- };
976
-
977
- // Upload parquet file
978
- const parquetResponse = await fetch(
979
- `https://huggingface.co/api/datasets/${repoId}/parquet`,
980
- {
981
- method: 'POST',
982
- headers: headers,
983
- body: JSON.stringify({
984
- file: 'data.parquet',
985
- content: parquetData
986
- })
987
- }
988
- );
989
-
990
- if (!parquetResponse.ok) {
991
- throw new Error('Failed to upload parquet file');
992
- }
993
-
994
- // Upload README
995
- const readmeResponse = await fetch(
996
- `https://huggingface.co/api/datasets/${repoId}/readme`,
997
- {
998
- method: 'POST',
999
- headers: headers,
1000
- body: JSON.stringify({
1001
- content: datasetCard
1002
- })
1003
- }
1004
- );
1005
-
1006
- if (!readmeResponse.ok) {
1007
- throw new Error('Failed to upload README');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  }
1009
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
 
1011
- // LocalStorage
1012
- function saveToLocalStorage() {
1013
- const data = {
1014
- text: state.text,
1015
- tracks: state.tracks,
1016
- colorIndex: colorIndex
1017
- };
1018
- localStorage.setItem('self_distillation_data', JSON.stringify(data));
1019
- }
 
1020
 
1021
- function loadFromLocalStorage() {
1022
- const saved = localStorage.getItem('self_distillation_data');
1023
- if (saved) {
1024
- try {
1025
- const data = JSON.parse(saved);
1026
- state.text = data.text || '';
1027
- state.tracks = data.tracks || [];
1028
- colorIndex = data.colorIndex || 0;
1029
- editor.value = state.text;
1030
- } catch (e) {
1031
- console.error('Failed to load saved data:', e);
1032
- }
1033
- }
1034
  }
1035
 
1036
- // Utility Functions
1037
- function hexToRgb(hex) {
1038
- const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
1039
- return result ? {
1040
- r: parseInt(result[1], 16),
1041
- g: parseInt(result[2], 16),
1042
- b: parseInt(result[3], 16)
1043
- } : { r: 0, g: 0, b: 0 };
1044
- }
1045
 
1046
- function showStatus(message, type) {
1047
- statusBar.textContent = message;
1048
- statusBar.className = `status-bar active ${type}`;
1049
-
1050
- setTimeout(() => {
1051
- statusBar.classList.remove('active');
1052
- }, 3000);
1053
- }
1054
 
1055
- // Initialize app
1056
- init();
1057
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1058
  </body>
1059
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Distill — Offline Emotion Dataset Builder</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg0: #0a0d13;
11
+ --bg1: #0f141d;
12
+ --bg2: #141b27;
13
+ --line: rgba(173, 188, 255, 0.12);
14
+ --line-strong: rgba(173, 188, 255, 0.2);
15
+ --text: #eaf0ff;
16
+ --muted: #95a3bd;
17
+ --accent: #7ea7ff;
18
+ --good: #7cf0b0;
19
+ --warn: #ffca74;
20
+ --danger: #ff7f98;
21
+ --shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
22
+ }
23
+
24
+ * { box-sizing: border-box; }
25
+ html, body { margin: 0; background:
26
+ radial-gradient(circle at top, rgba(126,167,255,0.08), transparent 40%),
27
+ linear-gradient(180deg, #0a0d13 0%, #0d121a 100%);
28
+ color: var(--text);
29
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
30
+ }
31
+
32
+ button, input, textarea {
33
+ font: inherit;
34
+ }
35
+
36
+ body {
37
+ min-height: 100vh;
38
+ }
39
+
40
+ #app {
41
+ max-width: 1500px;
42
+ margin: 0 auto;
43
+ padding: 16px;
44
+ display: grid;
45
+ gap: 14px;
46
+ }
47
+
48
+ .toolbar {
49
+ display: flex;
50
+ justify-content: space-between;
51
+ align-items: center;
52
+ gap: 12px;
53
+ flex-wrap: wrap;
54
+ padding: 12px;
55
+ border: 1px solid var(--line);
56
+ background: rgba(255,255,255,0.04);
57
+ backdrop-filter: blur(14px);
58
+ border-radius: 18px;
59
+ box-shadow: var(--shadow);
60
+ }
61
+
62
+ .group {
63
+ display: flex;
64
+ gap: 10px;
65
+ align-items: center;
66
+ flex-wrap: wrap;
67
+ }
68
+
69
+ .brand {
70
+ font-weight: 750;
71
+ letter-spacing: 0.02em;
72
+ padding-right: 4px;
73
+ }
74
+
75
+ button,
76
+ input[type="text"],
77
+ input[type="password"] {
78
+ border: 1px solid var(--line);
79
+ background: rgba(255,255,255,0.05);
80
+ color: var(--text);
81
+ border-radius: 12px;
82
+ transition: 120ms ease;
83
+ }
84
+
85
+ button {
86
+ padding: 10px 14px;
87
+ cursor: pointer;
88
+ }
89
+
90
+ button:hover {
91
+ border-color: var(--line-strong);
92
+ background: rgba(255,255,255,0.08);
93
+ transform: translateY(-1px);
94
+ }
95
+
96
+ button.primary {
97
+ background: linear-gradient(180deg, rgba(126,167,255,0.28), rgba(126,167,255,0.12));
98
+ border-color: rgba(126,167,255,0.45);
99
+ }
100
+
101
+ input[type="text"],
102
+ input[type="password"] {
103
+ padding: 10px 12px;
104
+ min-width: 220px;
105
+ outline: none;
106
+ }
107
+
108
+ input::placeholder,
109
+ textarea::placeholder {
110
+ color: #7e8ca5;
111
+ }
112
+
113
+ .editor-shell {
114
+ --aura: hsla(220, 80%, 60%, 0.12);
115
+ position: relative;
116
+ border: 1px solid var(--line);
117
+ border-radius: 24px;
118
+ overflow: hidden;
119
+ background:
120
+ radial-gradient(circle at top right, var(--aura), transparent 40%),
121
+ linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.02));
122
+ box-shadow:
123
+ 0 0 0 1px rgba(255,255,255,0.02),
124
+ 0 16px 48px rgba(0,0,0,0.32);
125
+ transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
126
+ }
127
+
128
+ #editor {
129
+ width: 100%;
130
+ min-height: 34vh;
131
+ resize: vertical;
132
+ border: 0;
133
+ outline: 0;
134
+ background: transparent;
135
+ color: var(--text);
136
+ padding: 24px 24px 8px;
137
+ line-height: 1.65;
138
+ font-size: 16px;
139
+ }
140
+
141
+ .active-bar {
142
+ min-height: 34px;
143
+ display: flex;
144
+ flex-wrap: wrap;
145
+ gap: 8px;
146
+ padding: 0 24px 18px;
147
+ align-items: center;
148
+ }
149
+
150
+ .pill {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 8px;
154
+ border: 1px solid var(--line);
155
+ padding: 6px 10px;
156
+ border-radius: 999px;
157
+ background: rgba(255,255,255,0.05);
158
+ font-size: 13px;
159
+ color: #dbe5ff;
160
+ }
161
+
162
+ .dot {
163
+ width: 10px;
164
+ height: 10px;
165
+ border-radius: 999px;
166
+ flex: 0 0 auto;
167
+ }
168
+
169
+ .muted {
170
+ color: var(--muted);
171
+ font-size: 13px;
172
+ }
173
+
174
+ .main-grid {
175
+ display: grid;
176
+ grid-template-columns: minmax(0, 1.25fr) minmax(330px, 0.85fr);
177
+ gap: 14px;
178
+ }
179
+
180
+ .panel {
181
+ border: 1px solid var(--line);
182
+ background: rgba(255,255,255,0.04);
183
+ border-radius: 20px;
184
+ overflow: hidden;
185
+ box-shadow: var(--shadow);
186
+ }
187
+
188
+ .panel-head {
189
+ padding: 14px 16px;
190
+ border-bottom: 1px solid rgba(255,255,255,0.06);
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ gap: 12px;
195
+ flex-wrap: wrap;
196
+ }
197
+
198
+ .meta {
199
+ color: var(--muted);
200
+ font-size: 12px;
201
+ }
202
+
203
+ .track-bar {
204
+ padding: 12px 16px 6px;
205
+ display: flex;
206
+ flex-wrap: wrap;
207
+ gap: 8px;
208
+ min-height: 52px;
209
+ align-items: center;
210
+ }
211
+
212
+ .track-chip {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ gap: 8px;
216
+ padding: 7px 10px;
217
+ border-radius: 999px;
218
+ border: 1px solid var(--line);
219
+ background: rgba(255,255,255,0.05);
220
+ color: var(--text);
221
+ cursor: pointer;
222
+ user-select: none;
223
+ font-size: 13px;
224
+ }
225
+
226
+ .track-chip:hover {
227
+ background: rgba(255,255,255,0.08);
228
+ border-color: var(--line-strong);
229
+ }
230
+
231
+ .track-chip .count {
232
+ color: var(--muted);
233
+ font-size: 12px;
234
+ }
235
+
236
+ .track-chip .close {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ width: 18px;
241
+ height: 18px;
242
+ border-radius: 999px;
243
+ background: rgba(255,255,255,0.08);
244
+ color: #dfe8ff;
245
+ font-size: 12px;
246
+ line-height: 1;
247
+ }
248
+
249
+ .timeline-wrap {
250
+ padding: 0 12px 12px;
251
+ }
252
+
253
+ #timeline {
254
+ display: block;
255
+ width: 100%;
256
+ height: 340px;
257
+ border-radius: 16px;
258
+ background:
259
+ linear-gradient(180deg, rgba(255,255,255,0.035), rgba(255,255,255,0.02));
260
+ cursor: crosshair;
261
+ }
262
+
263
+ .hint {
264
+ color: var(--muted);
265
+ font-size: 12px;
266
+ padding: 0 16px 14px;
267
+ }
268
+
269
+ pre {
270
+ margin: 0;
271
+ padding: 16px;
272
+ min-height: 340px;
273
+ max-height: 620px;
274
+ overflow: auto;
275
+ white-space: pre-wrap;
276
+ word-break: break-word;
277
+ color: #dce6ff;
278
+ font-size: 12px;
279
+ line-height: 1.5;
280
+ }
281
+
282
+ .footer-row {
283
+ display: flex;
284
+ justify-content: space-between;
285
+ gap: 12px;
286
+ flex-wrap: wrap;
287
+ padding: 2px 2px 10px;
288
+ color: var(--muted);
289
+ font-size: 12px;
290
+ }
291
+
292
+ .status {
293
+ position: fixed;
294
+ right: 16px;
295
+ bottom: 16px;
296
+ max-width: 420px;
297
+ padding: 12px 14px;
298
+ border-radius: 14px;
299
+ background: rgba(18, 24, 35, 0.95);
300
+ border: 1px solid rgba(255,255,255,0.1);
301
+ color: #eef4ff;
302
+ box-shadow: var(--shadow);
303
+ opacity: 0;
304
+ transform: translateY(6px);
305
+ pointer-events: none;
306
+ transition: 140ms ease;
307
+ z-index: 50;
308
+ white-space: pre-wrap;
309
+ }
310
+
311
+ .status.show {
312
+ opacity: 1;
313
+ transform: translateY(0);
314
+ }
315
+
316
+ .modal-backdrop {
317
+ position: fixed;
318
+ inset: 0;
319
+ display: none;
320
+ align-items: center;
321
+ justify-content: center;
322
+ background: rgba(0,0,0,0.5);
323
+ padding: 20px;
324
+ z-index: 60;
325
+ }
326
+
327
+ .modal-backdrop.show {
328
+ display: flex;
329
+ }
330
+
331
+ .modal {
332
+ width: min(560px, 100%);
333
+ border-radius: 18px;
334
+ overflow: hidden;
335
+ border: 1px solid var(--line);
336
+ background: #10151f;
337
+ box-shadow: var(--shadow);
338
+ }
339
+
340
+ .modal h3 {
341
+ margin: 0;
342
+ padding: 16px;
343
+ border-bottom: 1px solid rgba(255,255,255,0.06);
344
+ }
345
+
346
+ .modal .content {
347
+ padding: 16px;
348
+ display: grid;
349
+ gap: 12px;
350
+ }
351
+
352
+ label {
353
+ display: grid;
354
+ gap: 6px;
355
+ color: #c7d3ea;
356
+ font-size: 13px;
357
+ }
358
+
359
+ .empty {
360
+ color: var(--muted);
361
+ font-size: 13px;
362
+ padding: 6px 0;
363
+ }
364
+
365
+ .warning-note {
366
+ color: #f7d39a;
367
+ font-size: 12px;
368
+ line-height: 1.4;
369
+ }
370
+
371
+ @media (max-width: 1020px) {
372
+ .main-grid {
373
+ grid-template-columns: 1fr;
374
+ }
375
+ }
376
+ </style>
377
  </head>
378
  <body>
379
+ <div id="app">
380
+ <div class="toolbar">
381
+ <div class="group">
382
+ <div class="brand">Distill</div>
383
+ <button id="saveEntryBtn" class="primary">Save Entry</button>
384
+ <button id="exportBtn">Export Files</button>
385
+ <button id="uploadBtn">Push to HF</button>
386
+ <button id="settingsBtn">Settings</button>
387
+ </div>
388
+
389
+ <div class="group">
390
+ <input id="emotionNameInput" type="text" placeholder="Custom emotion…" />
391
+ <button id="addEmotionBtn">+ Track</button>
392
+ </div>
393
+ </div>
394
+
395
+ <div class="editor-shell" id="editorShell">
396
+ <textarea id="editor" placeholder="Write naturally. Then shape the emotional flow below."></textarea>
397
+ <div class="active-bar" id="activeBar"></div>
398
+ </div>
399
 
400
+ <div class="main-grid">
401
+ <section class="panel">
402
+ <div class="panel-head">
403
+ <strong>Emotion Timeline</strong>
404
+ <span class="meta" id="timelineMeta"></span>
 
405
  </div>
406
+ <div class="track-bar" id="trackBar"></div>
407
+ <div class="timeline-wrap">
408
+ <canvas id="timeline"></canvas>
409
+ </div>
410
+ <div class="hint">
411
+ Click a lane to drop a point • drag up/down for intensity • drag left/right for text offset •
412
+ double click or right click a point to delete • double click a track chip to rename •
413
+ Shift+click a lane to move the text cursor there
414
+ </div>
415
+ </section>
416
 
417
+ <section class="panel">
418
+ <div class="panel-head">
419
+ <strong>Live Dataset Preview</strong>
420
+ <span class="meta" id="entriesMeta"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  </div>
422
+ <pre id="jsonPreview"></pre>
423
+ </section>
424
  </div>
425
 
426
+ <div class="footer-row">
427
+ <div id="metaLine">0 chars • 0 tracks • 0 saved entries</div>
428
+ <div>Ctrl/Cmd+S save entry • Ctrl/Cmd+E focus custom emotion input</div>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="modal-backdrop" id="settingsBackdrop">
433
+ <div class="modal">
434
+ <h3>Hugging Face Settings</h3>
435
+ <div class="content">
436
+ <label>
437
+ Dataset ID
438
+ <input id="datasetIdInput" type="text" placeholder="username/dataset-name" />
439
+ </label>
440
+ <label>
441
+ Write Token
442
+ <input id="tokenInput" type="password" placeholder="hf_xxx" />
443
+ </label>
444
+ <div class="warning-note">
445
+ Stored only in your browser via localStorage. This app has no backend.
446
  </div>
447
+ <div class="group">
448
+ <button id="saveSettingsBtn" class="primary">Save Settings</button>
449
+ <button id="closeSettingsBtn">Close</button>
450
+ </div>
451
+ </div>
452
  </div>
453
+ </div>
454
+
455
+ <div class="status" id="status"></div>
456
+
457
+ <script>
458
+ (() => {
459
+ const STORAGE = {
460
+ settings: "distill.settings.v1",
461
+ draft: "distill.draft.v1",
462
+ entries: "distill.entries.v1"
463
+ };
464
+
465
+ const $ = (sel) => document.querySelector(sel);
466
+
467
+ const editor = $("#editor");
468
+ const editorShell = $("#editorShell");
469
+ const activeBar = $("#activeBar");
470
+ const emotionNameInput = $("#emotionNameInput");
471
+ const addEmotionBtn = $("#addEmotionBtn");
472
+ const saveEntryBtn = $("#saveEntryBtn");
473
+ const exportBtn = $("#exportBtn");
474
+ const uploadBtn = $("#uploadBtn");
475
+ const settingsBtn = $("#settingsBtn");
476
+ const timeline = $("#timeline");
477
+ const trackBar = $("#trackBar");
478
+ const jsonPreview = $("#jsonPreview");
479
+ const metaLine = $("#metaLine");
480
+ const timelineMeta = $("#timelineMeta");
481
+ const entriesMeta = $("#entriesMeta");
482
+
483
+ const settingsBackdrop = $("#settingsBackdrop");
484
+ const datasetIdInput = $("#datasetIdInput");
485
+ const tokenInput = $("#tokenInput");
486
+ const saveSettingsBtn = $("#saveSettingsBtn");
487
+ const closeSettingsBtn = $("#closeSettingsBtn");
488
+
489
+ const statusEl = $("#status");
490
+
491
+ const state = loadState();
492
+ let renderMeta = { lanes: [], points: [] };
493
+ let drag = null;
494
+ let pendingInput = null;
495
+ let toastTimer = null;
496
+
497
+ hydrate();
498
+ bind();
499
+ refreshAll();
500
+
501
+ function hydrate() {
502
+ editor.value = state.text;
503
+ datasetIdInput.value = state.settings.datasetId || "";
504
+ tokenInput.value = state.settings.token || "";
505
+ }
506
+
507
+ function bind() {
508
+ addEmotionBtn.addEventListener("click", onAddTrack);
509
+ emotionNameInput.addEventListener("keydown", (e) => {
510
+ if (e.key === "Enter") onAddTrack();
511
+ });
512
+
513
+ editor.addEventListener("beforeinput", () => {
514
+ pendingInput = {
515
+ oldText: state.text,
516
+ start: editor.selectionStart,
517
+ end: editor.selectionEnd
518
+ };
519
+ });
520
+
521
+ editor.addEventListener("input", () => {
522
+ const newText = editor.value;
523
+ if (pendingInput) {
524
+ shiftPointsForTextEdit(
525
+ pendingInput.start,
526
+ pendingInput.end,
527
+ newText.length - pendingInput.oldText.length,
528
+ newText.length
529
+ );
530
+ pendingInput = null;
531
+ }
532
+ state.text = newText;
533
+ persistDraft();
534
+ refreshLight();
535
+ });
536
+
537
+ ["click", "keyup", "mouseup", "select"].forEach((evt) => {
538
+ editor.addEventListener(evt, refreshLight);
539
+ });
540
+
541
+ timeline.addEventListener("mousedown", onTimelineDown);
542
+ window.addEventListener("mousemove", onTimelineMove);
543
+ window.addEventListener("mouseup", onTimelineUp);
544
+ timeline.addEventListener("dblclick", onTimelineDoubleClick);
545
+ timeline.addEventListener("contextmenu", onTimelineContextMenu);
546
+ window.addEventListener("resize", renderTimeline);
547
+
548
+ saveEntryBtn.addEventListener("click", saveEntry);
549
+ exportBtn.addEventListener("click", exportFiles);
550
+ uploadBtn.addEventListener("click", uploadToHuggingFace);
551
+ settingsBtn.addEventListener("click", () => openSettings(true));
552
+ saveSettingsBtn.addEventListener("click", saveSettings);
553
+ closeSettingsBtn.addEventListener("click", () => openSettings(false));
554
+ settingsBackdrop.addEventListener("click", (e) => {
555
+ if (e.target === settingsBackdrop) openSettings(false);
556
+ });
557
+
558
+ document.addEventListener("keydown", (e) => {
559
+ const mod = e.metaKey || e.ctrlKey;
560
+ if (mod && e.key.toLowerCase() === "s") {
561
+ e.preventDefault();
562
+ saveEntry();
563
+ }
564
+ if (mod && e.key.toLowerCase() === "e") {
565
+ e.preventDefault();
566
+ emotionNameInput.focus();
567
+ emotionNameInput.select();
568
+ }
569
+ if (e.key === "Escape") openSettings(false);
570
+ });
571
+ }
572
+
573
+ function loadState() {
574
+ const settings = loadJSON(STORAGE.settings, {});
575
+ const draft = loadJSON(STORAGE.draft, {});
576
+ const entries = loadJSON(STORAGE.entries, []);
577
+
578
+ return {
579
+ settings: {
580
+ datasetId: typeof settings.datasetId === "string" ? settings.datasetId : "",
581
+ token: typeof settings.token === "string" ? settings.token : ""
582
+ },
583
+ text: typeof draft.text === "string" ? draft.text : "",
584
+ emotions: sanitizeTracks(Array.isArray(draft.emotions) ? draft.emotions : []),
585
+ entries: Array.isArray(entries) ? entries : []
586
  };
587
+ }
588
+
589
+ function loadJSON(key, fallback) {
590
+ try {
591
+ const raw = localStorage.getItem(key);
592
+ return raw ? JSON.parse(raw) : fallback;
593
+ } catch {
594
+ return fallback;
595
+ }
596
+ }
597
+
598
+ function persistDraft() {
599
+ localStorage.setItem(
600
+ STORAGE.draft,
601
+ JSON.stringify({
602
+ text: state.text,
603
+ emotions: state.emotions
604
+ })
605
+ );
606
+ }
607
+
608
+ function persistEntries() {
609
+ localStorage.setItem(STORAGE.entries, JSON.stringify(state.entries));
610
+ }
611
+
612
+ function persistSettings() {
613
+ localStorage.setItem(STORAGE.settings, JSON.stringify(state.settings));
614
+ }
615
+
616
+ function sanitizeTracks(tracks) {
617
+ return tracks.map((t) => {
618
+ const name = String(t.name || "Emotion");
619
+ const hue = Number.isFinite(t.hue) ? t.hue : hueFor(name);
620
+ const points = Array.isArray(t.points) ? t.points.map((p) => ({
621
+ id: p.id || uid("pt"),
622
+ pos: clamp(Number(p.pos) || 0, 0, 999999999),
623
+ intensity: clamp(Number(p.intensity) || 0, 0, 1)
624
+ })) : [];
625
+
626
+ return {
627
+ id: t.id || uid("track"),
628
+ name,
629
+ hue,
630
+ color: colorFromHue(hue, 1, false),
631
+ points: points.sort((a, b) => a.pos - b.pos)
632
+ };
633
+ });
634
+ }
635
+
636
+ function uid(prefix = "id") {
637
+ return prefix + "_" + Math.random().toString(36).slice(2, 10);
638
+ }
639
+
640
+ function clamp(n, min, max) {
641
+ return Math.min(max, Math.max(min, n));
642
+ }
643
+
644
+ function round(n, digits = 3) {
645
+ const f = Math.pow(10, digits);
646
+ return Math.round(n * f) / f;
647
+ }
648
+
649
+ function hueFor(text) {
650
+ let h = 0;
651
+ for (let i = 0; i < text.length; i++) {
652
+ h = (h * 31 + text.charCodeAt(i)) % 360;
653
+ }
654
+ return h;
655
+ }
656
+
657
+ function colorFromHue(hue, alpha = 1, hslString = true) {
658
+ if (hslString) return `hsla(${hue}, 84%, 64%, ${alpha})`;
659
+ return `hsl(${hue}, 84%, 64%)`;
660
+ }
661
+
662
+ function shiftPointsForTextEdit(start, end, delta, newLength) {
663
+ const insertionOnly = start === end;
664
+
665
+ for (const track of state.emotions) {
666
+ for (const point of track.points) {
667
+ if (insertionOnly) {
668
+ if (point.pos >= start) point.pos += delta;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  } else {
670
+ if (point.pos > end) {
671
+ point.pos += delta;
672
+ } else if (point.pos > start && point.pos <= end) {
673
+ point.pos = start + Math.max(delta, 0);
674
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  }
676
+ point.pos = clamp(Math.round(point.pos), 0, newLength);
677
+ }
678
+ track.points.sort((a, b) => a.pos - b.pos);
679
+ }
680
+ }
681
+
682
+ function onAddTrack() {
683
+ const name = emotionNameInput.value.trim();
684
+ if (!name) return;
685
+
686
+ const exists = state.emotions.some((t) => t.name.toLowerCase() === name.toLowerCase());
687
+ if (exists) {
688
+ toast("That emotion track already exists.");
689
+ emotionNameInput.select();
690
+ return;
691
+ }
692
+
693
+ const hue = hueFor(name);
694
+ state.emotions.push({
695
+ id: uid("track"),
696
+ name,
697
+ hue,
698
+ color: colorFromHue(hue, 1, false),
699
+ points: []
700
+ });
701
+
702
+ emotionNameInput.value = "";
703
+ persistDraft();
704
+ refreshAll();
705
+ toast(`Added track: ${name}`);
706
+ }
707
+
708
+ function renameTrack(trackId) {
709
+ const track = state.emotions.find((t) => t.id === trackId);
710
+ if (!track) return;
711
+ const next = prompt("Rename emotion", track.name);
712
+ if (!next || !next.trim()) return;
713
+ track.name = next.trim();
714
+ track.hue = hueFor(track.name);
715
+ track.color = colorFromHue(track.hue, 1, false);
716
+ persistDraft();
717
+ refreshAll();
718
+ }
719
+
720
+ function removeTrack(trackId) {
721
+ const track = state.emotions.find((t) => t.id === trackId);
722
+ state.emotions = state.emotions.filter((t) => t.id !== trackId);
723
+ persistDraft();
724
+ refreshAll();
725
+ if (track) toast(`Removed track: ${track.name}`);
726
+ }
727
+
728
+ function renderTrackBar() {
729
+ trackBar.innerHTML = "";
730
+ if (!state.emotions.length) {
731
+ const empty = document.createElement("div");
732
+ empty.className = "empty";
733
+ empty.textContent = "No emotion tracks yet. Add a custom one above, then click its lane to paint emotion over the text.";
734
+ trackBar.appendChild(empty);
735
+ return;
736
+ }
737
+
738
+ for (const track of state.emotions) {
739
+ const chip = document.createElement("div");
740
+ chip.className = "track-chip";
741
+ chip.title = "Double click to rename";
742
+
743
+ const dot = document.createElement("span");
744
+ dot.className = "dot";
745
+ dot.style.background = colorFromHue(track.hue, 0.95);
746
+
747
+ const label = document.createElement("span");
748
+ label.textContent = track.name;
749
+
750
+ const count = document.createElement("span");
751
+ count.className = "count";
752
+ count.textContent = `${track.points.length} pts`;
753
+
754
+ const close = document.createElement("span");
755
+ close.className = "close";
756
+ close.textContent = "×";
757
+ close.title = "Remove track";
758
+
759
+ close.addEventListener("click", (e) => {
760
+ e.stopPropagation();
761
+ removeTrack(track.id);
762
+ });
763
+
764
+ chip.addEventListener("dblclick", () => renameTrack(track.id));
765
+
766
+ chip.append(dot, label, count, close);
767
+ trackBar.appendChild(chip);
768
+ }
769
+ }
770
+
771
+ function currentCursorPos() {
772
+ return typeof editor.selectionStart === "number" ? editor.selectionStart : 0;
773
+ }
774
+
775
+ function intensityAt(track, pos) {
776
+ const points = [...track.points].sort((a, b) => a.pos - b.pos);
777
+ if (!points.length) return 0;
778
+ if (points.length === 1) return pos === points[0].pos ? points[0].intensity : 0;
779
+ if (pos < points[0].pos || pos > points[points.length - 1].pos) return 0;
780
+
781
+ for (let i = 0; i < points.length; i++) {
782
+ const a = points[i];
783
+ const b = points[i + 1];
784
+
785
+ if (pos === a.pos) return a.intensity;
786
+ if (!b) continue;
787
+
788
+ if (a.pos === b.pos && pos === a.pos) {
789
+ return b.intensity;
790
+ }
791
+
792
+ if (pos >= a.pos && pos <= b.pos) {
793
+ const span = Math.max(1, b.pos - a.pos);
794
+ const t = (pos - a.pos) / span;
795
+ return a.intensity + (b.intensity - a.intensity) * t;
796
+ }
797
+ }
798
+
799
+ return 0;
800
+ }
801
+
802
+ function getActiveEmotionsAt(pos) {
803
+ return state.emotions
804
+ .map((track) => ({
805
+ id: track.id,
806
+ name: track.name,
807
+ hue: track.hue,
808
+ intensity: round(intensityAt(track, pos), 3)
809
+ }))
810
+ .filter((x) => x.intensity > 0.001)
811
+ .sort((a, b) => b.intensity - a.intensity);
812
+ }
813
+
814
+ function renderActiveBar() {
815
+ activeBar.innerHTML = "";
816
+ const cursor = currentCursorPos();
817
+ const active = getActiveEmotionsAt(cursor);
818
+
819
+ if (!active.length) {
820
+ const muted = document.createElement("div");
821
+ muted.className = "muted";
822
+ muted.textContent = state.emotions.length
823
+ ? "No active emotion at the current cursor position."
824
+ : "Add a custom emotion track, then click in the timeline to shape how you felt while writing.";
825
+ activeBar.appendChild(muted);
826
+
827
+ editorShell.style.setProperty("--aura", "hsla(220, 80%, 60%, 0.12)");
828
+ editorShell.style.borderColor = "rgba(173, 188, 255, 0.12)";
829
+ editorShell.style.boxShadow =
830
+ "0 0 0 1px rgba(255,255,255,0.02), 0 16px 48px rgba(0,0,0,0.32)";
831
+ return;
832
+ }
833
+
834
+ for (const item of active) {
835
+ const pill = document.createElement("div");
836
+ pill.className = "pill";
837
+
838
+ const dot = document.createElement("span");
839
+ dot.className = "dot";
840
+ dot.style.background = colorFromHue(item.hue, 0.95);
841
+
842
+ const label = document.createElement("span");
843
+ label.textContent = `${item.name} ${Math.round(item.intensity * 100)}%`;
844
+
845
+ pill.append(dot, label);
846
+ activeBar.appendChild(pill);
847
+ }
848
+
849
+ const dominant = active[0];
850
+ editorShell.style.setProperty("--aura", colorFromHue(dominant.hue, 0.16 + dominant.intensity * 0.26));
851
+ editorShell.style.borderColor = colorFromHue(dominant.hue, 0.18 + dominant.intensity * 0.28);
852
+ editorShell.style.boxShadow =
853
+ `0 0 0 1px rgba(255,255,255,0.02),
854
+ 0 16px 48px rgba(0,0,0,0.32),
855
+ 0 0 ${18 + dominant.intensity * 34}px ${colorFromHue(dominant.hue, 0.1 + dominant.intensity * 0.28)}`;
856
+ }
857
+
858
+ function renderTimeline() {
859
+ const dpr = window.devicePixelRatio || 1;
860
+ const cssWidth = timeline.clientWidth || timeline.parentElement.clientWidth || 800;
861
+ const cssHeight = Math.max(240, state.emotions.length * 86 + 28);
862
+ timeline.style.height = cssHeight + "px";
863
+ timeline.width = Math.floor(cssWidth * dpr);
864
+ timeline.height = Math.floor(cssHeight * dpr);
865
+
866
+ const ctx = timeline.getContext("2d");
867
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
868
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
869
+
870
+ renderMeta = { lanes: [], points: [] };
871
+
872
+ const pad = 14;
873
+ const gutter = 152;
874
+ const laneHeight = 86;
875
+ const graphHeight = 52;
876
+ const graphLeft = gutter;
877
+ const graphRight = cssWidth - pad;
878
+ const graphWidth = Math.max(10, graphRight - graphLeft);
879
+ const textLength = Math.max(state.text.length, 1);
880
+ const cursor = currentCursorPos();
881
+
882
+ if (!state.emotions.length) {
883
+ ctx.fillStyle = "rgba(255,255,255,0.08)";
884
+ roundRect(ctx, pad, pad, cssWidth - pad * 2, cssHeight - pad * 2, 16);
885
+ ctx.fill();
886
+
887
+ ctx.fillStyle = "rgba(255,255,255,0.65)";
888
+ ctx.font = "600 15px Inter, system-ui, sans-serif";
889
+ ctx.fillText("No tracks yet", 24, 42);
890
+ ctx.fillStyle = "rgba(255,255,255,0.45)";
891
+ ctx.font = "13px Inter, system-ui, sans-serif";
892
+ ctx.fillText("Add a custom emotion above. Then click here to place intensity points over the text.", 24, 66);
893
+ return;
894
+ }
895
+
896
+ for (let i = 0; i < state.emotions.length; i++) {
897
+ const track = state.emotions[i];
898
+ const laneY = pad + i * laneHeight;
899
+ const graphTop = laneY + 18;
900
+ const graphBottom = graphTop + graphHeight;
901
+ const labelX = 24;
902
+ const laneW = cssWidth - pad * 2;
903
+ const laneH = laneHeight - 10;
904
+
905
+ renderMeta.lanes.push({
906
+ trackId: track.id,
907
+ graphLeft,
908
+ graphRight,
909
+ graphWidth,
910
+ graphTop,
911
+ graphBottom,
912
+ graphHeight,
913
+ laneY,
914
+ laneH
915
+ });
916
+
917
+ ctx.fillStyle = "rgba(255,255,255,0.03)";
918
+ roundRect(ctx, pad, laneY, laneW, laneH, 14);
919
+ ctx.fill();
920
+
921
+ ctx.fillStyle = "rgba(255,255,255,0.04)";
922
+ roundRect(ctx, graphLeft - 8, graphTop - 6, graphWidth + 8, graphHeight + 12, 12);
923
+ ctx.fill();
924
+
925
+ ctx.strokeStyle = "rgba(255,255,255,0.06)";
926
+ ctx.lineWidth = 1;
927
+ for (const ratio of [0, 0.25, 0.5, 0.75, 1]) {
928
+ const y = graphBottom - ratio * graphHeight;
929
  ctx.beginPath();
930
+ ctx.moveTo(graphLeft, y);
931
+ ctx.lineTo(graphRight, y);
 
 
 
 
 
 
 
 
932
  ctx.stroke();
933
+ }
934
+
935
+ const cursorX = graphLeft + (cursor / textLength) * graphWidth;
936
+ ctx.strokeStyle = "rgba(255,255,255,0.18)";
937
+ ctx.lineWidth = 1;
938
+ ctx.beginPath();
939
+ ctx.moveTo(cursorX, graphTop - 6);
940
+ ctx.lineTo(cursorX, graphBottom + 6);
941
+ ctx.stroke();
942
+
943
+ ctx.fillStyle = colorFromHue(track.hue, 0.95);
944
+ ctx.beginPath();
945
+ ctx.arc(labelX, laneY + 24, 5, 0, Math.PI * 2);
946
+ ctx.fill();
947
+
948
+ ctx.fillStyle = "rgba(240,245,255,0.95)";
949
+ ctx.font = "600 14px Inter, system-ui, sans-serif";
950
+ ctx.fillText(track.name, labelX + 14, laneY + 29);
951
+
952
+ const currentIntensity = intensityAt(track, cursor);
953
+ ctx.fillStyle = "rgba(255,255,255,0.46)";
954
+ ctx.font = "12px Inter, system-ui, sans-serif";
955
+ ctx.fillText(
956
+ `${Math.round(currentIntensity * 100)}%`,
957
+ graphLeft - 42,
958
+ laneY + 29
959
+ );
960
+
961
+ const points = [...track.points].sort((a, b) => a.pos - b.pos);
962
+
963
+ if (points.length) {
964
+ const xOf = (pos) => graphLeft + (clamp(pos, 0, textLength) / textLength) * graphWidth;
965
+ const yOf = (intensity) => graphBottom - clamp(intensity, 0, 1) * graphHeight;
966
+
967
+ const first = points[0];
968
+ const last = points[points.length - 1];
969
+
970
+ ctx.beginPath();
971
+ ctx.moveTo(xOf(first.pos), graphBottom);
972
+ ctx.lineTo(xOf(first.pos), yOf(first.intensity));
973
+ for (const p of points) {
974
+ ctx.lineTo(xOf(p.pos), yOf(p.intensity));
975
+ }
976
+ ctx.lineTo(xOf(last.pos), graphBottom);
977
  ctx.closePath();
978
+ ctx.fillStyle = colorFromHue(track.hue, 0.16);
979
  ctx.fill();
 
 
 
 
 
 
 
980
 
 
 
 
 
 
981
  ctx.beginPath();
982
+ ctx.moveTo(xOf(first.pos), yOf(first.intensity));
983
+ for (let j = 1; j < points.length; j++) {
984
+ ctx.lineTo(xOf(points[j].pos), yOf(points[j].intensity));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
  }
986
+ ctx.strokeStyle = colorFromHue(track.hue, 0.9);
 
 
 
 
 
 
 
987
  ctx.lineWidth = 2;
 
 
 
 
988
  ctx.stroke();
 
 
989
 
990
+ for (const p of points) {
991
+ const px = xOf(p.pos);
992
+ const py = yOf(p.intensity);
993
+
994
+ renderMeta.points.push({
995
+ trackId: track.id,
996
+ pointId: p.id,
997
+ x: px,
998
+ y: py
999
+ });
1000
+
1001
+ ctx.beginPath();
1002
+ ctx.arc(px, py, 6.4, 0, Math.PI * 2);
1003
+ ctx.fillStyle = colorFromHue(track.hue, 1);
1004
+ ctx.fill();
1005
+ ctx.strokeStyle = "rgba(8,10,14,0.95)";
1006
+ ctx.lineWidth = 1.5;
1007
+ ctx.stroke();
 
 
 
 
 
 
 
 
 
 
 
 
1008
  }
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ function roundRect(ctx, x, y, w, h, r) {
1014
+ const rr = Math.min(r, w / 2, h / 2);
1015
+ ctx.beginPath();
1016
+ ctx.moveTo(x + rr, y);
1017
+ ctx.arcTo(x + w, y, x + w, y + h, rr);
1018
+ ctx.arcTo(x + w, y + h, x, y + h, rr);
1019
+ ctx.arcTo(x, y + h, x, y, rr);
1020
+ ctx.arcTo(x, y, x + w, y, rr);
1021
+ ctx.closePath();
1022
+ }
1023
+
1024
+ function getMouseInCanvas(e) {
1025
+ const rect = timeline.getBoundingClientRect();
1026
+ return {
1027
+ x: e.clientX - rect.left,
1028
+ y: e.clientY - rect.top
1029
+ };
1030
+ }
1031
+
1032
+ function findLaneAt(x, y) {
1033
+ return renderMeta.lanes.find((lane) => {
1034
+ const insideY = y >= lane.laneY && y <= lane.laneY + lane.laneH;
1035
+ const insideX = x >= lane.graphLeft - 8 && x <= lane.graphRight + 4;
1036
+ return insideX && insideY;
1037
+ }) || null;
1038
+ }
1039
+
1040
+ function findPointHit(x, y) {
1041
+ let best = null;
1042
+ let bestDist = Infinity;
1043
+ for (const pt of renderMeta.points) {
1044
+ const dx = pt.x - x;
1045
+ const dy = pt.y - y;
1046
+ const d = Math.sqrt(dx * dx + dy * dy);
1047
+ if (d <= 10 && d < bestDist) {
1048
+ best = pt;
1049
+ bestDist = d;
1050
+ }
1051
+ }
1052
+ return best;
1053
+ }
1054
+
1055
+ function laneXToPos(lane, x) {
1056
+ const fraction = clamp((x - lane.graphLeft) / lane.graphWidth, 0, 1);
1057
+ return Math.round(fraction * state.text.length);
1058
+ }
1059
+
1060
+ function laneYToIntensity(lane, y) {
1061
+ return clamp((lane.graphBottom - y) / lane.graphHeight, 0, 1);
1062
+ }
1063
+
1064
+ function getTrack(trackId) {
1065
+ return state.emotions.find((t) => t.id === trackId);
1066
+ }
1067
+
1068
+ function getPoint(track, pointId) {
1069
+ return track.points.find((p) => p.id === pointId);
1070
+ }
1071
+
1072
+ function addPoint(track, pos, intensity) {
1073
+ const point = {
1074
+ id: uid("pt"),
1075
+ pos: clamp(Math.round(pos), 0, state.text.length),
1076
+ intensity: clamp(intensity, 0, 1)
1077
+ };
1078
+ track.points.push(point);
1079
+ track.points.sort((a, b) => a.pos - b.pos);
1080
+ return point;
1081
+ }
1082
+
1083
+ function onTimelineDown(e) {
1084
+ const { x, y } = getMouseInCanvas(e);
1085
+ const hit = findPointHit(x, y);
1086
+
1087
+ if (hit) {
1088
+ drag = { trackId: hit.trackId, pointId: hit.pointId };
1089
+ e.preventDefault();
1090
+ return;
1091
+ }
1092
+
1093
+ const lane = findLaneAt(x, y);
1094
+ if (!lane || x < lane.graphLeft || x > lane.graphRight) return;
1095
+
1096
+ if (e.shiftKey) {
1097
+ const pos = laneXToPos(lane, x);
1098
+ editor.focus();
1099
+ editor.setSelectionRange(pos, pos);
1100
+ refreshLight();
1101
+ return;
1102
+ }
1103
+
1104
+ const track = getTrack(lane.trackId);
1105
+ if (!track) return;
1106
+
1107
+ const point = addPoint(track, laneXToPos(lane, x), laneYToIntensity(lane, y));
1108
+ drag = { trackId: track.id, pointId: point.id };
1109
+ persistDraft();
1110
+ refreshAll();
1111
+ e.preventDefault();
1112
+ }
1113
+
1114
+ function onTimelineMove(e) {
1115
+ if (!drag) return;
1116
+
1117
+ const { x, y } = getMouseInCanvas(e);
1118
+ const lane = renderMeta.lanes.find((l) => l.trackId === drag.trackId);
1119
+ const track = getTrack(drag.trackId);
1120
+ if (!lane || !track) return;
1121
+
1122
+ const point = getPoint(track, drag.pointId);
1123
+ if (!point) return;
1124
+
1125
+ point.pos = clamp(laneXToPos(lane, x), 0, state.text.length);
1126
+ point.intensity = clamp(laneYToIntensity(lane, y), 0, 1);
1127
+ track.points.sort((a, b) => a.pos - b.pos);
1128
+
1129
+ refreshLight();
1130
+ }
1131
+
1132
+ function onTimelineUp() {
1133
+ if (!drag) return;
1134
+ drag = null;
1135
+ persistDraft();
1136
+ refreshLight();
1137
+ }
1138
+
1139
+ function deletePointAtMouse(e) {
1140
+ const { x, y } = getMouseInCanvas(e);
1141
+ const hit = findPointHit(x, y);
1142
+ if (!hit) return false;
1143
+ const track = getTrack(hit.trackId);
1144
+ if (!track) return false;
1145
+ track.points = track.points.filter((p) => p.id !== hit.pointId);
1146
+ persistDraft();
1147
+ refreshAll();
1148
+ return true;
1149
+ }
1150
+
1151
+ function onTimelineDoubleClick(e) {
1152
+ if (deletePointAtMouse(e)) {
1153
+ e.preventDefault();
1154
+ }
1155
+ }
1156
+
1157
+ function onTimelineContextMenu(e) {
1158
+ if (deletePointAtMouse(e)) {
1159
+ e.preventDefault();
1160
+ }
1161
+ }
1162
+
1163
+ function buildFlowForTrack(track, textLength) {
1164
+ const pts = [...track.points]
1165
+ .sort((a, b) => a.pos - b.pos)
1166
+ .map((p) => ({
1167
+ char_index: Math.round(p.pos),
1168
+ intensity: round(clamp(p.intensity, 0, 1), 3)
1169
+ }));
1170
+
1171
+ const anchors = [{ char_index: 0, intensity: 0 }, ...pts, { char_index: textLength, intensity: 0 }];
1172
+ const segments = [];
1173
+
1174
+ for (let i = 0; i < anchors.length - 1; i++) {
1175
+ const a = anchors[i];
1176
+ const b = anchors[i + 1];
1177
+ if (a.char_index === b.char_index && a.intensity === b.intensity) continue;
1178
+ segments.push({
1179
+ start_char: a.char_index,
1180
+ end_char: b.char_index,
1181
+ start_intensity: round(a.intensity, 3),
1182
+ end_intensity: round(b.intensity, 3)
1183
+ });
1184
+ }
1185
+
1186
+ return {
1187
+ label: track.name,
1188
+ color: track.color,
1189
+ points: pts,
1190
+ segments
1191
+ };
1192
+ }
1193
+
1194
+ function buildEntry({ preview = false } = {}) {
1195
+ return {
1196
+ id: preview ? "draft_preview" : uid("entry"),
1197
+ created_at: preview ? null : new Date().toISOString(),
1198
+ text: state.text,
1199
+ text_length: state.text.length,
1200
+ emotional_flow: state.emotions
1201
+ .map((track) => buildFlowForTrack(track, state.text.length))
1202
+ .filter((flow) => flow.points.length > 0)
1203
+ };
1204
+ }
1205
+
1206
+ function renderPreview() {
1207
+ jsonPreview.textContent = JSON.stringify(buildEntry({ preview: true }), null, 2);
1208
+ entriesMeta.textContent = state.entries.length
1209
+ ? `${state.entries.length} saved entr${state.entries.length === 1 ? "y" : "ies"}`
1210
+ : "current draft preview";
1211
+ }
1212
+
1213
+ function updateMeta() {
1214
+ const chars = state.text.length;
1215
+ const tracks = state.emotions.length;
1216
+ const entries = state.entries.length;
1217
+ const cursor = currentCursorPos();
1218
+
1219
+ metaLine.textContent = `${chars} chars • ${tracks} tracks • ${entries} saved entries`;
1220
+ timelineMeta.textContent = `cursor ${cursor}/${chars}`;
1221
+ }
1222
+
1223
+ function refreshAll() {
1224
+ renderTrackBar();
1225
+ renderActiveBar();
1226
+ renderPreview();
1227
+ renderTimeline();
1228
+ updateMeta();
1229
+ }
1230
+
1231
+ function refreshLight() {
1232
+ renderActiveBar();
1233
+ renderPreview();
1234
+ renderTimeline();
1235
+ updateMeta();
1236
+ }
1237
+
1238
+ function saveEntry() {
1239
+ if (!state.text.trim()) {
1240
+ toast("Nothing to save yet.");
1241
+ return;
1242
+ }
1243
+
1244
+ const entry = buildEntry({ preview: false });
1245
+ state.entries.push(entry);
1246
+ persistEntries();
1247
+ renderPreview();
1248
+ updateMeta();
1249
+ toast(`Saved entry #${state.entries.length}`);
1250
+ }
1251
+
1252
+ function rowsForExport() {
1253
+ if (state.entries.length) return state.entries;
1254
+ if (state.text.trim()) return [buildEntry({ preview: false })];
1255
+ return [];
1256
+ }
1257
+
1258
+ function toJSONL(rows) {
1259
+ return rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
1260
+ }
1261
+
1262
+ function generateReadme(rows) {
1263
+ const datasetId = state.settings.datasetId || "user/distill-dataset";
1264
+ const example = rows[0] || buildEntry({ preview: true });
1265
+
1266
+ return `---
1267
+ pretty_name: ${datasetId.split("/").pop()}
1268
+ license: other
1269
+ tags:
1270
+ - emotion
1271
+ - journaling
1272
+ - self-distillation
1273
+ - text
1274
+ ---
1275
 
1276
+ # ${datasetId}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
 
1278
+ Offline-generated writing dataset with character-aligned emotional automation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
 
1280
+ ## Description
 
 
 
 
 
 
 
 
 
 
 
 
 
1281
 
1282
+ This dataset stores free-form writing along with custom emotion curves mapped over character offsets.
1283
+ Each emotion has control points and derived linear segments, making it suitable for studying emotional flow during writing.
 
 
 
 
 
 
 
 
1284
 
1285
+ ## Row schema
 
 
1286
 
1287
+ - \`id\`
1288
+ - \`created_at\`
1289
+ - \`text\`
1290
+ - \`text_length\`
1291
+ - \`emotional_flow[]\`
1292
+ - \`label\`
1293
+ - \`color\`
1294
+ - \`points[]\`
1295
+ - \`char_index\`
1296
+ - \`intensity\`
1297
+ - \`segments[]\`
1298
+ - \`start_char\`
1299
+ - \`end_char\`
1300
+ - \`start_intensity\`
1301
+ - \`end_intensity\`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1302
 
1303
+ ## Number of rows
1304
+
1305
+ ${rows.length}
1306
+
1307
+ ## Example
1308
+
1309
+ \`\`\`json
1310
+ ${JSON.stringify(example, null, 2)}
1311
+ \`\`\`
1312
+ `;
1313
+ }
 
 
 
 
 
 
 
 
 
 
 
1314
 
1315
+ function generateJinjaTemplate() {
1316
+ return `{% set dataset_name = dataset_name | default("distill-dataset") %}
1317
+ ---
1318
+ pretty_name: {{ dataset_name }}
1319
+ license: other
 
 
1320
  tags:
1321
+ - emotion
1322
+ - journaling
1323
  - self-distillation
1324
+ - text
 
1325
  ---
1326
 
1327
+ # {{ dataset_name }}
1328
 
1329
+ Generated with Distill.
 
1330
 
1331
+ ## Description
 
 
 
 
1332
 
1333
+ Character-aligned emotional curves layered over text.
 
 
1334
 
1335
+ ## Fields
 
 
 
 
 
 
 
1336
 
1337
+ - \`id\`
1338
+ - \`created_at\`
1339
+ - \`text\`
1340
+ - \`text_length\`
1341
+ - \`emotional_flow[].label\`
1342
+ - \`emotional_flow[].points[].char_index\`
1343
+ - \`emotional_flow[].points[].intensity\`
1344
+ - \`emotional_flow[].segments[]\`
1345
 
1346
+ ## Example
1347
+
1348
+ {% if example %}
1349
+ \`\`\`json
1350
+ {{ example | tojson(indent=2) }}
1351
+ \`\`\`
1352
+ {% endif %}
1353
+ `;
1354
+ }
1355
+
1356
+ function generateDatasetInfos(rows, jsonl) {
1357
+ return {
1358
+ default: {
1359
+ description: "Personal self-distillation dataset with custom emotional curves aligned to text offsets.",
1360
+ citation: "",
1361
+ homepage: "",
1362
+ license: "",
1363
+ features: {
1364
+ id: { dtype: "string", _type: "Value" },
1365
+ created_at: { dtype: "string", _type: "Value" },
1366
+ text: { dtype: "string", _type: "Value" },
1367
+ text_length: { dtype: "int32", _type: "Value" },
1368
+ emotional_flow: {
1369
+ feature: {
1370
+ label: { dtype: "string", _type: "Value" },
1371
+ color: { dtype: "string", _type: "Value" },
1372
+ points: {
1373
+ feature: {
1374
+ char_index: { dtype: "int32", _type: "Value" },
1375
+ intensity: { dtype: "float32", _type: "Value" }
1376
+ },
1377
+ _type: "Sequence"
1378
+ },
1379
+ segments: {
1380
+ feature: {
1381
+ start_char: { dtype: "int32", _type: "Value" },
1382
+ end_char: { dtype: "int32", _type: "Value" },
1383
+ start_intensity: { dtype: "float32", _type: "Value" },
1384
+ end_intensity: { dtype: "float32", _type: "Value" }
1385
+ },
1386
+ _type: "Sequence"
1387
+ }
1388
+ },
1389
+ _type: "Sequence"
1390
+ }
1391
+ },
1392
+ splits: {
1393
+ train: {
1394
+ name: "train",
1395
+ num_examples: rows.length,
1396
+ num_bytes: new Blob([jsonl]).size
1397
+ }
1398
  }
1399
+ }
1400
+ };
1401
+ }
1402
+
1403
+ function textFile(path, text, type = "text/plain") {
1404
+ return {
1405
+ path,
1406
+ name: path.replaceAll("/", "__"),
1407
+ type,
1408
+ blob: new Blob([text], { type })
1409
+ };
1410
+ }
1411
+
1412
+ function buildExportFiles() {
1413
+ const rows = rowsForExport();
1414
+ if (!rows.length) throw new Error("No saved entries or draft text to export.");
1415
+
1416
+ const jsonl = toJSONL(rows);
1417
+ const readme = generateReadme(rows);
1418
+ const jinja = generateJinjaTemplate();
1419
+ const datasetInfos = JSON.stringify(generateDatasetInfos(rows, jsonl), null, 2);
1420
+ const draftPreview = JSON.stringify(buildEntry({ preview: true }), null, 2);
1421
+
1422
+ // Minimal HF-ready export package.
1423
+ // Parquet is not included in this no-dependency single-file MVP.
1424
+ return [
1425
+ textFile("data/train.jsonl", jsonl, "application/json"),
1426
+ textFile("README.md", readme, "text/markdown"),
1427
+ textFile("card_template.jinja2", jinja, "text/plain"),
1428
+ textFile("dataset_infos.json", datasetInfos, "application/json"),
1429
+ textFile("draft_preview.json", draftPreview, "application/json")
1430
+ ];
1431
+ }
1432
+
1433
+ function downloadBlob(name, blob) {
1434
+ const url = URL.createObjectURL(blob);
1435
+ const a = document.createElement("a");
1436
+ a.href = url;
1437
+ a.download = name;
1438
+ document.body.appendChild(a);
1439
+ a.click();
1440
+ a.remove();
1441
+ setTimeout(() => URL.revokeObjectURL(url), 500);
1442
+ }
1443
+
1444
+ function exportFiles() {
1445
+ try {
1446
+ const files = buildExportFiles();
1447
+ files.forEach((file, i) => {
1448
+ setTimeout(() => downloadBlob(file.name, file.blob), i * 120);
1449
+ });
1450
+ toast("Exported HF-ready files.\n(JSONL + README + Jinja2 + dataset_infos)");
1451
+ } catch (err) {
1452
+ toast(err.message || "Export failed.");
1453
+ }
1454
+ }
1455
+
1456
+ function openSettings(show) {
1457
+ settingsBackdrop.classList.toggle("show", !!show);
1458
+ if (show) {
1459
+ datasetIdInput.value = state.settings.datasetId || "";
1460
+ tokenInput.value = state.settings.token || "";
1461
+ }
1462
+ }
1463
+
1464
+ function saveSettings() {
1465
+ state.settings.datasetId = datasetIdInput.value.trim();
1466
+ state.settings.token = tokenInput.value.trim();
1467
+ persistSettings();
1468
+ openSettings(false);
1469
+ toast("Settings saved locally.");
1470
+ }
1471
+
1472
+ function encodeRepoPath(path) {
1473
+ return path.split("/").map(encodeURIComponent).join("/");
1474
+ }
1475
+
1476
+ async function maybeCreateRepo(datasetId, token) {
1477
+ const parts = datasetId.split("/");
1478
+ if (parts.length !== 2) return;
1479
+ const [owner, name] = parts;
1480
+
1481
+ const tries = [
1482
+ { type: "dataset", name, organization: owner, private: false },
1483
+ { type: "dataset", name, private: false }
1484
+ ];
1485
 
1486
+ for (const body of tries) {
1487
+ try {
1488
+ const res = await fetch("https://huggingface.co/api/repos/create", {
1489
+ method: "POST",
1490
+ headers: {
1491
+ "Authorization": `Bearer ${token}`,
1492
+ "Content-Type": "application/json"
1493
+ },
1494
+ body: JSON.stringify(body)
1495
+ });
1496
 
1497
+ if (res.ok || res.status === 409) return true;
1498
+ } catch (_) {}
 
 
 
 
 
 
 
 
 
 
 
1499
  }
1500
 
1501
+ return false;
1502
+ }
 
 
 
 
 
 
 
1503
 
1504
+ async function uploadFileToHF(datasetId, token, file) {
1505
+ const path = encodeRepoPath(file.path);
1506
+ const urls = [
1507
+ `https://huggingface.co/api/datasets/${datasetId}/upload/main/${path}`,
1508
+ `https://huggingface.co/api/datasets/${encodeURIComponent(datasetId)}/upload/main/${path}`
1509
+ ];
 
 
1510
 
1511
+ let lastError = "Unknown upload error.";
1512
+
1513
+ for (const url of urls) {
1514
+ for (const method of ["POST", "PUT"]) {
1515
+ try {
1516
+ const res = await fetch(url, {
1517
+ method,
1518
+ headers: {
1519
+ "Authorization": `Bearer ${token}`,
1520
+ "Content-Type": file.type
1521
+ },
1522
+ body: file.blob
1523
+ });
1524
+
1525
+ if (res.ok) return true;
1526
+ lastError = `${method} ${url}\n${res.status} ${await safeText(res)}`;
1527
+ } catch (err) {
1528
+ lastError = `${method} ${url}\n${err.message || err}`;
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ throw new Error(lastError);
1534
+ }
1535
+
1536
+ async function safeText(res) {
1537
+ try {
1538
+ return await res.text();
1539
+ } catch {
1540
+ return "";
1541
+ }
1542
+ }
1543
+
1544
+ async function uploadToHuggingFace() {
1545
+ const datasetId = state.settings.datasetId;
1546
+ const token = state.settings.token;
1547
+
1548
+ if (!datasetId || !token) {
1549
+ openSettings(true);
1550
+ toast("Set Dataset ID and HF token first.");
1551
+ return;
1552
+ }
1553
+
1554
+ let files;
1555
+ try {
1556
+ files = buildExportFiles();
1557
+ } catch (err) {
1558
+ toast(err.message || "Nothing to upload.");
1559
+ return;
1560
+ }
1561
+
1562
+ toast("Uploading to Hugging Face…");
1563
+ await maybeCreateRepo(datasetId, token);
1564
+
1565
+ try {
1566
+ for (const file of files) {
1567
+ await uploadFileToHF(datasetId, token, file);
1568
+ }
1569
+ toast(`Uploaded ${files.length} files to ${datasetId}`);
1570
+ } catch (err) {
1571
+ toast(
1572
+ "Upload failed.\n" +
1573
+ (err.message || String(err)) +
1574
+ "\n\nYou can still use Export Files and upload manually."
1575
+ );
1576
+ }
1577
+ }
1578
+
1579
+ function toast(message) {
1580
+ statusEl.textContent = message;
1581
+ statusEl.classList.add("show");
1582
+ clearTimeout(toastTimer);
1583
+ toastTimer = setTimeout(() => {
1584
+ statusEl.classList.remove("show");
1585
+ }, 3200);
1586
+ }
1587
+ })();
1588
+ </script>
1589
  </body>
1590
  </html>