wop commited on
Commit
a163f01
·
verified ·
1 Parent(s): 3e3b5e1

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +980 -205
index.html CHANGED
@@ -5,6 +5,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;
@@ -12,13 +14,17 @@
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 {
@@ -33,8 +39,8 @@
33
  display: flex;
34
  flex-direction: column;
35
  height: 100vh;
36
- padding: 20px;
37
- gap: 15px;
38
  }
39
 
40
  /* Header */
@@ -42,41 +48,94 @@
42
  display: flex;
43
  justify-content: space-between;
44
  align-items: center;
45
- padding: 15px;
46
  background: var(--bg-secondary);
47
- border-radius: 12px;
48
  border: 1px solid var(--border);
49
  flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
  .header h1 {
53
- font-size: 1.5rem;
54
  font-weight: 600;
55
- background: linear-gradient(135deg, #6366f1, #8b5cf6);
56
  -webkit-background-clip: text;
57
  -webkit-text-fill-color: transparent;
 
58
  }
59
 
60
  .header-controls {
61
  display: flex;
62
- gap: 10px;
63
  }
64
 
65
  .btn {
66
  padding: 8px 16px;
67
  border: none;
68
- border-radius: 6px;
69
  cursor: pointer;
70
- font-size: 0.9rem;
71
- transition: all 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
- .btn-primary { background: var(--accent); color: white; }
75
- .btn-primary:hover { background: #5558e3; transform: translateY(-1px); }
76
- .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
77
- .btn-secondary:hover { background: #252535; }
78
- .btn-danger { background: #7f1d1d; color: #fca5a5; border: 1px solid #991b1b; }
79
- .btn-danger:hover { background: #991b1b; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  /* Editor Section */
82
  .editor-section {
@@ -85,24 +144,88 @@
85
  flex-direction: column;
86
  position: relative;
87
  min-height: 200px;
 
88
  }
89
 
90
  .editor-wrapper {
91
  flex: 1;
92
  position: relative;
93
- border-radius: 12px;
94
  border: 1px solid var(--border);
95
- background: var(--bg-secondary);
96
  overflow: hidden;
97
- transition: box-shadow 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
  #editorOverlay {
101
  position: absolute;
102
  top: 0; left: 0; right: 0; bottom: 0;
103
- padding: 20px;
104
- font-size: 1.1rem;
105
- line-height: 1.6;
106
  font-family: inherit;
107
  color: var(--text-primary);
108
  white-space: pre-wrap;
@@ -111,27 +234,57 @@
111
  overflow-y: auto;
112
  pointer-events: none;
113
  z-index: 1;
 
114
  }
115
 
116
  #editorOverlay .word {
117
  pointer-events: auto;
118
  cursor: pointer;
119
- transition: background-color 0.2s, border-radius 0.2s;
120
- padding: 0 2px;
121
- margin: 0 -2px;
122
- border-radius: 3px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
  #editor {
126
  width: 100%;
127
  height: 100%;
128
  background: transparent;
129
- color: transparent; /* Hide standard text */
130
- caret-color: var(--text-primary); /* Keep cursor visible */
131
  border: none;
132
- padding: 20px;
133
- font-size: 1.1rem;
134
- line-height: 1.6;
135
  font-family: inherit;
136
  resize: none;
137
  outline: none;
@@ -141,15 +294,47 @@
141
  white-space: pre-wrap;
142
  word-break: break-word;
143
  overflow-wrap: break-word;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
 
146
  /* Tracks List */
147
  .tracks-container {
148
  display: flex;
149
- gap: 10px;
150
  flex-wrap: wrap;
151
- padding: 0 5px;
152
  min-height: 20px;
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
  .track-item {
@@ -157,24 +342,48 @@
157
  align-items: center;
158
  gap: 6px;
159
  background: var(--bg-tertiary);
160
- padding: 4px 10px;
161
- border-radius: 16px;
162
- font-size: 0.85rem;
163
  border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  }
165
 
166
  .track-color {
167
- width: 10px;
168
- height: 10px;
169
  border-radius: 50%;
 
 
 
 
 
 
 
 
 
170
  }
171
 
172
  .track-delete {
173
  cursor: pointer;
174
- color: var(--text-secondary);
175
  font-weight: bold;
176
- margin-left: 4px;
177
  transition: color 0.2s;
 
 
178
  }
179
 
180
  .track-delete:hover { color: #ef4444; }
@@ -182,9 +391,9 @@
182
  /* Timeline Section */
183
  .timeline-section {
184
  background: var(--bg-secondary);
185
- border-radius: 12px;
186
  border: 1px solid var(--border);
187
- padding: 20px;
188
  flex-shrink: 0;
189
  }
190
 
@@ -192,126 +401,300 @@
192
  display: flex;
193
  justify-content: space-between;
194
  align-items: center;
195
- margin-bottom: 15px;
196
  flex-wrap: wrap;
197
  gap: 10px;
198
  }
199
 
200
- .timeline-title { font-size: 1.1rem; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- .emotion-input-container { display: flex; gap: 10px; align-items: center; }
 
 
 
 
203
 
204
  #emotionInput {
205
- padding: 8px 12px;
206
  background: var(--bg-tertiary);
207
  border: 1px solid var(--border);
208
- border-radius: 6px;
209
  color: var(--text-primary);
210
- font-size: 0.9rem;
211
  width: 200px;
 
 
 
 
 
 
 
212
  }
213
 
214
  .timeline-canvas-container {
215
  position: relative;
216
  background: var(--bg-tertiary);
217
- border-radius: 8px;
218
  overflow: hidden;
219
  cursor: crosshair;
 
220
  }
221
 
222
- #timelineCanvas { display: block; width: 100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  .instructions {
225
- margin-top: 15px;
226
- padding: 12px;
227
  background: var(--bg-tertiary);
228
- border-radius: 6px;
229
- font-size: 0.8rem;
 
 
 
 
 
 
230
  color: var(--text-secondary);
231
- line-height: 1.5;
232
  }
233
 
234
- .instructions strong { color: var(--text-primary); }
 
 
 
 
 
 
 
 
 
235
 
236
  /* Modal */
237
  .modal {
238
  display: none;
239
  position: fixed;
240
  inset: 0;
241
- background: rgba(0, 0, 0, 0.8);
242
  z-index: 1000;
243
  justify-content: center;
244
  align-items: center;
 
245
  }
246
 
247
  .modal.active { display: flex; }
248
 
249
  .modal-content {
250
  background: var(--bg-secondary);
251
- border-radius: 12px;
252
  padding: 30px;
253
- max-width: 500px;
254
  width: 90%;
255
  border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
 
258
- .modal-content h2 { margin-bottom: 20px; font-size: 1.3rem; }
 
 
 
259
 
260
- .form-group { margin-bottom: 15px; }
261
- .form-group label { display: block; margin-bottom: 5px; font-size: 0.9rem; color: var(--text-secondary); }
262
- .form-group input { width: 100%; padding: 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 0.9rem; }
263
- .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
 
 
264
 
265
  /* Status */
266
  .status-bar {
267
  margin-top: 10px;
268
- padding: 10px;
269
  background: var(--bg-tertiary);
270
- border-radius: 6px;
271
- font-size: 0.85rem;
272
  color: var(--text-secondary);
273
  display: none;
 
 
 
 
 
 
 
274
  }
 
275
  .status-bar.active { display: block; }
276
- .status-bar.success { border-left: 3px solid #10b981; }
277
- .status-bar.error { border-left: 3px solid #ef4444; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  </style>
279
  </head>
280
  <body>
281
  <div class="container">
282
  <div class="header">
283
- <h1>Self-Distillation</h1>
 
 
 
284
  <div class="header-controls">
285
- <button class="btn btn-secondary" onclick="exportJSON()">Export JSON</button>
286
- <button class="btn btn-danger" onclick="clearAll()">Clear All</button>
287
- <button class="btn btn-secondary" onclick="showHFModal()">Push to HF</button>
 
 
 
 
 
 
288
  </div>
289
  </div>
290
 
291
  <div class="editor-section">
292
  <div class="editor-wrapper" id="editorWrapper">
 
 
293
  <div id="editorOverlay"></div>
294
- <textarea id="editor" placeholder="Write your thoughts here..."></textarea>
 
 
 
 
 
 
 
 
 
 
295
  </div>
296
- <div class="tracks-container" id="tracksList"></div>
297
  </div>
298
 
299
  <div class="timeline-section">
300
  <div class="timeline-header">
301
- <div class="timeline-title">Emotional Timeline (Per Word)</div>
 
 
 
302
  <div class="emotion-input-container">
303
- <input type="text" id="emotionInput" placeholder="Enter emotion (e.g., Joy, Anxiety)">
304
- <button class="btn btn-primary" onclick="addEmotionTrack()">Add Track</button>
305
  </div>
306
  </div>
307
-
308
  <div class="timeline-canvas-container">
309
  <canvas id="timelineCanvas"></canvas>
310
  </div>
311
 
312
  <div class="instructions">
313
- <strong>Controls:</strong> Click timeline to add point Drag vertically for intensity • Drag handles for fades •
314
- <strong>Hover</strong> points/words to see emotion • <strong>Double-click</strong> point to remove • <strong>Click</strong> colored word to jump cursor
 
 
 
 
315
  </div>
316
 
317
  <div class="status-bar" id="statusBar"></div>
@@ -321,7 +704,7 @@
321
  <div class="modal" id="hfModal">
322
  <div class="modal-content">
323
  <h2>Push to HuggingFace</h2>
324
- <p style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 15px;">This will fetch existing data and append your new entry without overwriting.</p>
325
  <div class="form-group">
326
  <label>HF Token</label>
327
  <input type="password" id="hfToken" placeholder="hf_...">
@@ -348,43 +731,121 @@
348
  dragging: false,
349
  dragType: null,
350
  dragStart: { x: 0, y: 0 },
351
- cursorWordIndex: 0
 
 
352
  };
353
 
354
  // DOM Elements
355
  const editor = document.getElementById('editor');
356
  const editorOverlay = document.getElementById('editorOverlay');
357
  const editorWrapper = document.getElementById('editorWrapper');
 
 
 
358
  const canvas = document.getElementById('timelineCanvas');
359
  const ctx = canvas.getContext('2d');
360
  const emotionInput = document.getElementById('emotionInput');
361
  const statusBar = document.getElementById('statusBar');
362
  const tracksList = document.getElementById('tracksList');
363
-
364
- const colorPalette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#a855f7'];
 
 
 
 
 
 
 
 
365
  let colorIndex = 0;
366
 
367
- // Tooltip Setup
368
  const tooltip = document.createElement('div');
369
- tooltip.style.cssText = 'display:none; position:fixed; background:var(--bg-tertiary); color:var(--text-primary); padding:5px 10px; border-radius:4px; font-size:12px; pointer-events:none; z-index:100; border:1px solid var(--border); box-shadow: 0 4px 6px rgba(0,0,0,0.3);';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  document.body.appendChild(tooltip);
371
 
372
- function showTooltip(text, x, y) {
373
- tooltip.textContent = text;
374
  tooltip.style.display = 'block';
375
- tooltip.style.left = (x + 15) + 'px';
376
- tooltip.style.top = (y - 10) + 'px';
 
 
 
 
 
377
  }
378
  function hideTooltip() { tooltip.style.display = 'none'; }
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  // Initialization
381
  function init() {
382
  loadFromLocalStorage();
383
  setupEventListeners();
384
  resizeCanvas();
 
385
  renderTracksList();
386
  updateOverlay();
387
  render();
 
388
  }
389
 
390
  function setupEventListeners() {
@@ -395,14 +856,22 @@
395
  editorOverlay.scrollTop = editor.scrollTop;
396
  editorOverlay.scrollLeft = editor.scrollLeft;
397
  });
398
-
399
  canvas.addEventListener('mousedown', handleCanvasMouseDown);
400
  canvas.addEventListener('mousemove', handleCanvasMouseMove);
401
  canvas.addEventListener('mouseup', handleCanvasMouseUp);
402
  canvas.addEventListener('mouseleave', () => { handleCanvasMouseUp(); hideTooltip(); });
403
  canvas.addEventListener('dblclick', handleCanvasDoubleClick);
404
-
405
- window.addEventListener('resize', () => { resizeCanvas(); render(); });
 
 
 
 
 
 
 
 
406
  }
407
 
408
  // Text Handling
@@ -419,7 +888,6 @@
419
  function handleTextChange(e) {
420
  state.text = e.target.value;
421
  state.words = getWords(state.text);
422
- // Clamp points if text is shortened
423
  state.tracks.forEach(track => {
424
  track.points = track.points.filter(p => p.wordIndex < state.words.length);
425
  });
@@ -430,7 +898,7 @@
430
 
431
  function handleEditorClick() {
432
  const charIndex = editor.selectionStart;
433
- let wordIdx = state.words.length;
434
  for (let i = 0; i < state.words.length; i++) {
435
  if (charIndex <= state.words[i].end) {
436
  wordIdx = i;
@@ -438,24 +906,35 @@
438
  }
439
  }
440
  state.cursorWordIndex = wordIdx;
441
- updateEditorGlow();
442
  render();
443
  }
444
 
445
  function updateOverlay() {
446
  let html = '';
447
  let lastIndex = 0;
448
-
 
 
 
 
 
 
449
  state.words.forEach((word, i) => {
450
- const space = state.text.substring(lastIndex, word.start).replace(/ /g, '&nbsp;').replace(/\n/g, '<br>').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
 
 
 
451
  html += space;
452
-
453
- let bgStyle = '';
454
  const emotionsAtWord = getActiveEmotionsAtWord(i);
 
 
455
  let titleText = '';
456
-
 
457
  if (emotionsAtWord.length > 0) {
458
- let r=0, g=0, b=0, total=0;
459
  emotionsAtWord.forEach(e => {
460
  const rgb = hexToRgb(e.color);
461
  r += rgb.r * e.intensity;
@@ -463,24 +942,39 @@
463
  b += rgb.b * e.intensity;
464
  total += e.intensity;
465
  });
466
- if(total > 0) {
467
- r = Math.round(r/total); g = Math.round(g/total); b = Math.round(b/total);
 
 
468
  const maxInt = Math.max(...emotionsAtWord.map(e => e.intensity));
469
- bgStyle = `background-color: rgba(${r}, ${g}, ${b}, ${Math.max(0.2, maxInt * 0.6)});`;
470
- titleText = emotionsAtWord.map(e => `${e.label} (${e.intensity.toFixed(2)})`).join(', ');
 
 
 
 
 
 
 
 
471
  }
472
  }
473
-
474
  const escapedText = word.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
475
- html += `<span class="word" data-index="${i}" style="${bgStyle}" title="${titleText}">${escapedText}</span>`;
 
476
  lastIndex = word.end;
477
  });
478
-
479
- const trailing = state.text.substring(lastIndex).replace(/ /g, '&nbsp;').replace(/\n/g, '<br>').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
 
 
 
480
  html += trailing;
481
-
482
- editorOverlay.innerHTML = html.trim() ? html : '<span style="color:var(--text-secondary)">Write your thoughts here...</span>';
483
-
 
484
  document.querySelectorAll('#editorOverlay .word').forEach(span => {
485
  span.addEventListener('click', (e) => {
486
  const wordIndex = parseInt(span.getAttribute('data-index'));
@@ -490,44 +984,57 @@
490
  handleEditorClick();
491
  e.stopPropagation();
492
  });
493
-
494
  span.addEventListener('mousemove', (e) => {
495
  const wordIndex = parseInt(span.getAttribute('data-index'));
496
  const emotions = getActiveEmotionsAtWord(wordIndex);
497
- if (emotions.length > 0) showTooltip(emotions.map(em => `${em.label} (${em.intensity.toFixed(2)})`).join(', '), e.clientX, e.clientY);
498
- else hideTooltip();
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  });
500
  span.addEventListener('mouseleave', hideTooltip);
501
  });
502
  }
503
 
504
- // Canvas Setup & Coordinates
505
  function resizeCanvas() {
506
  const container = canvas.parentElement;
507
  canvas.width = container.clientWidth;
508
- canvas.height = 200;
509
  }
510
 
511
  function wordIndexToX(wordIndex) {
512
  const totalWords = state.words.length || 1;
513
- const padding = 20, availableWidth = canvas.width - (padding * 2);
514
  return padding + (wordIndex / totalWords) * availableWidth;
515
  }
516
 
517
  function xToWordIndex(x) {
518
- const padding = 20, availableWidth = canvas.width - (padding * 2);
519
  const totalWords = state.words.length || 1;
520
  const normalizedX = Math.max(0, Math.min(x - padding, availableWidth));
521
  return Math.round((normalizedX / availableWidth) * totalWords);
522
  }
523
 
524
  function intensityToY(intensity) {
525
- const padding = 20, availableHeight = canvas.height - (padding * 2);
526
  return padding + availableHeight - (intensity * availableHeight);
527
  }
528
 
529
  function yToIntensity(y) {
530
- const padding = 20, availableHeight = canvas.height - (padding * 2);
531
  const normalizedY = Math.max(padding, Math.min(y, canvas.height - padding));
532
  return 1 - ((normalizedY - padding) / availableHeight);
533
  }
@@ -541,22 +1048,32 @@
541
  function handleCanvasMouseDown(e) {
542
  const coords = getCanvasCoordinates(e);
543
  const clickedItem = findItemAtPosition(coords.x, coords.y);
544
-
545
  if (clickedItem) {
546
  state.selectedPoint = clickedItem;
547
  state.dragging = true;
548
  state.dragType = clickedItem.type;
549
  state.dragStart = { x: coords.x, y: coords.y };
550
  } else {
551
- const activeTrack = state.tracks[state.tracks.length - 1];
 
552
  if (activeTrack) {
553
  const wordIndex = xToWordIndex(coords.x);
554
- const newPoint = { id: Date.now(), wordIndex: wordIndex, intensity: Math.max(0, Math.min(1, yToIntensity(coords.y))), fadeIn: 0, fadeOut: 0 };
 
 
 
 
 
 
555
  activeTrack.points.push(newPoint);
556
  activeTrack.points.sort((a, b) => a.wordIndex - b.wordIndex);
557
  state.selectedPoint = { type: 'point', point: newPoint, track: activeTrack };
558
- state.dragging = true; state.dragType = 'point';
559
- render(); updateOverlay(); saveToLocalStorage();
 
 
 
560
  }
561
  }
562
  }
@@ -566,8 +1083,17 @@
566
  if (!state.dragging) {
567
  const item = findItemAtPosition(coords.x, coords.y);
568
  if (item && item.type === 'point') {
569
- canvas.style.cursor = 'pointer';
570
- showTooltip(`${item.track.label} (Intensity: ${item.point.intensity.toFixed(2)})`, e.clientX, e.clientY);
 
 
 
 
 
 
 
 
 
571
  } else {
572
  canvas.style.cursor = 'crosshair';
573
  hideTooltip();
@@ -576,6 +1102,7 @@
576
  }
577
 
578
  if (!state.selectedPoint) return;
 
579
  const deltaX = coords.x - state.dragStart.x;
580
 
581
  if (state.dragType === 'point') {
@@ -591,18 +1118,26 @@
591
  }
592
 
593
  state.dragStart = { x: coords.x, y: coords.y };
594
- render(); updateOverlay(); saveToLocalStorage();
 
 
595
  }
596
 
597
- function handleCanvasMouseUp() { state.dragging = false; state.dragType = null; }
 
 
 
 
598
 
599
  function handleCanvasDoubleClick(e) {
600
  const coords = getCanvasCoordinates(e);
601
  const clickedItem = findItemAtPosition(coords.x, coords.y);
602
  if (clickedItem && clickedItem.type === 'point') {
603
  clickedItem.track.points = clickedItem.track.points.filter(p => p.id !== clickedItem.point.id);
604
- render(); updateOverlay(); saveToLocalStorage();
605
- showStatus('Emotion point removed', 'success');
 
 
606
  }
607
  }
608
 
@@ -633,72 +1168,240 @@
633
  }
634
 
635
  function drawGrid() {
636
- ctx.strokeStyle = '#2a2a3a'; ctx.lineWidth = 1;
637
- const step = Math.max(1, Math.floor(state.words.length / 20));
638
- for (let i = 0; i <= state.words.length; i += step) {
639
- const x = wordIndexToX(i);
640
- ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
641
- }
642
  for (let i = 0; i <= 1; i += 0.25) {
643
  const y = intensityToY(i);
644
- ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
  }
646
  }
647
 
648
  function drawTrack(track) {
649
  if (track.points.length === 0) return;
650
- ctx.strokeStyle = track.color; ctx.fillStyle = track.color; ctx.lineWidth = 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  ctx.beginPath();
652
  track.points.forEach((point, index) => {
653
- const x = wordIndexToX(point.wordIndex), y = intensityToY(point.intensity);
654
- index === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
 
 
 
 
 
 
 
 
 
655
  });
 
 
 
 
 
656
  ctx.stroke();
657
-
658
- ctx.globalAlpha = 0.1;
659
- ctx.lineTo(wordIndexToX(track.points[track.points.length - 1].wordIndex), canvas.height - 20);
660
- ctx.lineTo(wordIndexToX(track.points[0].wordIndex), canvas.height - 20);
661
- ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
662
-
663
  track.points.forEach(point => drawPoint(point, track));
664
  }
665
 
666
  function drawPoint(point, track) {
667
- const x = wordIndexToX(point.wordIndex), y = intensityToY(point.intensity);
668
- ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fillStyle = track.color; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
669
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  if (point.fadeIn > 0) {
671
  const fadeInX = wordIndexToX(point.wordIndex - point.fadeIn);
672
- ctx.beginPath(); ctx.arc(fadeInX, y, 4, 0, Math.PI * 2); ctx.fillStyle = track.color; ctx.fill();
673
- ctx.beginPath(); ctx.moveTo(fadeInX, y); ctx.lineTo(x, y); ctx.strokeStyle = track.color; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.stroke(); ctx.setLineDash([]);
 
 
 
 
 
 
 
 
 
 
674
  }
675
  if (point.fadeOut > 0) {
676
  const fadeOutX = wordIndexToX(point.wordIndex + point.fadeOut);
677
- ctx.beginPath(); ctx.arc(fadeOutX, y, 4, 0, Math.PI * 2); ctx.fillStyle = track.color; ctx.fill();
678
- ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(fadeOutX, y); ctx.strokeStyle = track.color; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.stroke(); ctx.setLineDash([]);
 
 
 
 
 
 
 
 
 
 
679
  }
680
  }
681
 
682
  function drawCursorIndicator() {
683
  if (state.words.length === 0) return;
684
  const x = wordIndexToX(state.cursorWordIndex);
685
- ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]);
686
- ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); ctx.setLineDash([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  }
688
 
689
- function updateEditorGlow() {
690
  const activeEmotions = getActiveEmotionsAtWord(state.cursorWordIndex);
691
- if (activeEmotions.length === 0) { editorWrapper.style.boxShadow = 'none'; return; }
692
-
693
- let r=0, g=0, b=0, total=0;
 
 
 
 
 
 
 
 
 
 
 
694
  activeEmotions.forEach(e => {
695
  const rgb = hexToRgb(e.color);
696
- r += rgb.r * e.intensity; g += rgb.g * e.intensity; b += rgb.b * e.intensity; total += e.intensity;
 
 
 
697
  });
698
- if(total > 0) {
699
- r = Math.round(r/total); g = Math.round(g/total); b = Math.round(b/total);
 
 
 
700
  const avgInt = total / activeEmotions.length;
701
- editorWrapper.style.boxShadow = `0 0 15px rgba(${r}, ${g}, ${b}, ${avgInt * 0.6})`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  }
703
  }
704
 
@@ -706,44 +1409,86 @@
706
  const activeEmotions = [];
707
  state.tracks.forEach(track => {
708
  track.points.forEach(point => {
709
- const start = point.wordIndex - point.fadeIn, end = point.wordIndex + point.fadeOut;
 
710
  if (wordIndex >= start && wordIndex <= end) {
711
  let intensity = point.intensity;
712
- if (wordIndex < point.wordIndex && point.fadeIn > 0) intensity *= (wordIndex - start) / point.fadeIn;
713
- if (wordIndex > point.wordIndex && point.fadeOut > 0) intensity *= 1 - ((wordIndex - point.wordIndex) / point.fadeOut);
714
- activeEmotions.push({ label: track.label, color: track.color, intensity: Math.max(0, Math.min(1, intensity)) });
 
 
 
 
 
 
715
  }
716
  });
717
  });
718
  return activeEmotions;
719
  }
720
 
721
- // Tracks Management
722
  function addEmotionTrack() {
723
  const emotionName = emotionInput.value.trim();
724
  if (!emotionName) { showStatus('Please enter an emotion name', 'error'); return; }
725
- state.tracks.push({ id: Date.now(), label: emotionName, color: colorPalette[colorIndex++ % colorPalette.length], points: [] });
 
 
 
 
 
 
 
726
  emotionInput.value = '';
727
- renderTracksList(); render(); saveToLocalStorage();
728
- showStatus(`Added track: ${emotionName}`, 'success');
 
 
729
  }
730
 
731
  function renderTracksList() {
732
- tracksList.innerHTML = '';
 
 
 
 
733
  state.tracks.forEach(track => {
 
734
  const item = document.createElement('div');
735
- item.className = 'track-item';
736
- item.innerHTML = `<div class="track-color" style="background:${track.color}"></div><span>${track.label}</span><span class="track-delete" onclick="deleteTrack(${track.id})">&times;</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  tracksList.appendChild(item);
738
  });
739
  }
740
 
741
  function deleteTrack(trackId) {
742
  state.tracks = state.tracks.filter(t => t.id !== trackId);
743
- renderTracksList(); render(); updateOverlay(); saveToLocalStorage();
 
 
 
 
 
 
744
  }
745
 
746
- // Data Export & Clear
747
  function generateDataset() {
748
  const emotionalFlow = [];
749
  state.tracks.forEach(track => {
@@ -763,20 +1508,36 @@
763
  const jsonStr = JSON.stringify(generateDataset(), null, 2);
764
  const blob = new Blob([jsonStr], { type: 'application/json' });
765
  const a = document.createElement('a');
766
- a.href = URL.createObjectURL(blob); a.download = 'self-distillation-dataset.json'; a.click();
 
 
767
  showStatus('Dataset exported successfully', 'success');
768
  }
769
 
770
  function clearAll() {
771
- if (confirm('Are you sure you want to clear all text and emotion tracks? (HF credentials will be kept)')) {
772
  localStorage.removeItem('self_distillation_data');
773
- state.text = ''; state.tracks = []; state.words = []; state.cursorWordIndex = 0;
774
- editor.value = ''; updateOverlay(); renderTracksList(); render();
775
- showStatus('All local data cleared', 'success');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  }
777
  }
778
 
779
- // HuggingFace Integration
780
  function showHFModal() {
781
  const savedToken = localStorage.getItem('hf_token');
782
  const savedRepo = localStorage.getItem('hf_repo_id');
@@ -791,12 +1552,12 @@
791
  const token = document.getElementById('hfToken').value.trim();
792
  const repoId = document.getElementById('hfRepoId').value.trim();
793
  if (!token || !repoId) { showStatus('Please provide both token and repository ID', 'error'); return; }
794
-
795
  localStorage.setItem('hf_token', token);
796
  localStorage.setItem('hf_repo_id', repoId);
797
-
798
  try {
799
- showStatus('Fetching existing dataset to append...', 'success');
800
  let existingData = [];
801
  try {
802
  const res = await fetch(`https://huggingface.co/datasets/${repoId}/resolve/main/data.json`);
@@ -805,11 +1566,11 @@
805
  existingData = JSON.parse(text);
806
  if (!Array.isArray(existingData)) existingData = [existingData];
807
  }
808
- } catch(e) { console.log('No existing file, creating new.'); }
809
 
810
  existingData.push(generateDataset());
811
 
812
- showStatus('Uploading to HuggingFace...', 'success');
813
  const commitUrl = `https://huggingface.co/api/datasets/${repoId}/commit/main`;
814
  const response = await fetch(commitUrl, {
815
  method: "POST",
@@ -819,7 +1580,7 @@
819
  operations: [{ key: "data.json", operation: "addOrUpdate", content: JSON.stringify(existingData, null, 2) }]
820
  })
821
  });
822
-
823
  if (!response.ok) throw new Error('Failed to commit to HF');
824
  showStatus('Successfully appended to HuggingFace!', 'success');
825
  closeHFModal();
@@ -828,9 +1589,14 @@
828
  }
829
  }
830
 
831
- // LocalStorage & Utils
832
  function saveToLocalStorage() {
833
- localStorage.setItem('self_distillation_data', JSON.stringify({ text: state.text, tracks: state.tracks, colorIndex }));
 
 
 
 
 
834
  }
835
 
836
  function loadFromLocalStorage() {
@@ -838,19 +1604,28 @@
838
  if (saved) {
839
  try {
840
  const data = JSON.parse(saved);
841
- state.text = data.text || ''; state.tracks = data.tracks || []; colorIndex = data.colorIndex || 0;
842
- state.words = getWords(state.text); editor.value = state.text;
843
- } catch (e) { console.error('Failed to load saved data:', e); }
 
 
 
 
844
  }
845
  }
846
 
847
  function hexToRgb(hex) {
848
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
849
- return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : { r: 0, g: 0, b: 0 };
 
 
 
 
850
  }
851
 
852
  function showStatus(message, type) {
853
- statusBar.textContent = message; statusBar.className = `status-bar active ${type}`;
 
854
  setTimeout(() => statusBar.classList.remove('active'), 3000);
855
  }
856
 
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Self-Distillation</title>
7
  <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
9
+
10
  * {
11
  margin: 0;
12
  padding: 0;
 
14
  }
15
 
16
  :root {
17
+ --bg-primary: #08080d;
18
+ --bg-secondary: #0e0e16;
19
+ --bg-tertiary: #16161f;
20
+ --bg-card: #111119;
21
+ --text-primary: #e8e8f0;
22
+ --text-secondary: #6b6b80;
23
+ --text-muted: #44445a;
24
  --accent: #6366f1;
25
+ --accent-glow: rgba(99, 102, 241, 0.15);
26
+ --border: #1e1e2e;
27
+ --border-subtle: #181828;
28
  }
29
 
30
  body {
 
39
  display: flex;
40
  flex-direction: column;
41
  height: 100vh;
42
+ padding: 16px;
43
+ gap: 12px;
44
  }
45
 
46
  /* Header */
 
48
  display: flex;
49
  justify-content: space-between;
50
  align-items: center;
51
+ padding: 14px 20px;
52
  background: var(--bg-secondary);
53
+ border-radius: 14px;
54
  border: 1px solid var(--border);
55
  flex-shrink: 0;
56
+ backdrop-filter: blur(10px);
57
+ }
58
+
59
+ .header-left {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 12px;
63
+ }
64
+
65
+ .header-icon {
66
+ width: 36px;
67
+ height: 36px;
68
+ border-radius: 10px;
69
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ font-size: 1.1rem;
74
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
75
  }
76
 
77
  .header h1 {
78
+ font-size: 1.3rem;
79
  font-weight: 600;
80
+ background: linear-gradient(135deg, #c7c8ff, #8b5cf6);
81
  -webkit-background-clip: text;
82
  -webkit-text-fill-color: transparent;
83
+ letter-spacing: -0.02em;
84
  }
85
 
86
  .header-controls {
87
  display: flex;
88
+ gap: 8px;
89
  }
90
 
91
  .btn {
92
  padding: 8px 16px;
93
  border: none;
94
+ border-radius: 8px;
95
  cursor: pointer;
96
+ font-size: 0.82rem;
97
+ font-weight: 500;
98
+ transition: all 0.2s ease;
99
+ font-family: inherit;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 6px;
103
+ }
104
+
105
+ .btn-primary {
106
+ background: linear-gradient(135deg, #6366f1, #7c3aed);
107
+ color: white;
108
+ box-shadow: 0 2px 10px rgba(99, 102, 241, 0.3);
109
+ }
110
+ .btn-primary:hover {
111
+ background: linear-gradient(135deg, #5558e3, #6d28d9);
112
+ transform: translateY(-1px);
113
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
114
  }
115
 
116
+ .btn-secondary {
117
+ background: var(--bg-tertiary);
118
+ color: var(--text-primary);
119
+ border: 1px solid var(--border);
120
+ }
121
+ .btn-secondary:hover {
122
+ background: #1e1e2e;
123
+ border-color: #2a2a40;
124
+ }
125
+
126
+ .btn-danger {
127
+ background: rgba(127, 29, 29, 0.5);
128
+ color: #fca5a5;
129
+ border: 1px solid rgba(153, 27, 27, 0.5);
130
+ }
131
+ .btn-danger:hover {
132
+ background: rgba(153, 27, 27, 0.6);
133
+ }
134
+
135
+ .btn-icon {
136
+ font-size: 1rem;
137
+ line-height: 1;
138
+ }
139
 
140
  /* Editor Section */
141
  .editor-section {
 
144
  flex-direction: column;
145
  position: relative;
146
  min-height: 200px;
147
+ gap: 8px;
148
  }
149
 
150
  .editor-wrapper {
151
  flex: 1;
152
  position: relative;
153
+ border-radius: 16px;
154
  border: 1px solid var(--border);
155
+ background: var(--bg-card);
156
  overflow: hidden;
157
+ transition: border-color 0.4s ease, box-shadow 0.4s ease;
158
+ }
159
+
160
+ .editor-wrapper:focus-within {
161
+ border-color: rgba(99, 102, 241, 0.3);
162
+ }
163
+
164
+ /* Ambient glow behind editor */
165
+ .editor-ambient {
166
+ position: absolute;
167
+ top: -50%;
168
+ left: -50%;
169
+ width: 200%;
170
+ height: 200%;
171
+ pointer-events: none;
172
+ z-index: 0;
173
+ opacity: 0;
174
+ transition: opacity 0.6s ease;
175
+ filter: blur(80px);
176
+ }
177
+
178
+ .editor-wrapper.has-emotion .editor-ambient {
179
+ opacity: 1;
180
+ }
181
+
182
+ /* Word count / stats bar */
183
+ .editor-stats {
184
+ position: absolute;
185
+ bottom: 0;
186
+ left: 0;
187
+ right: 0;
188
+ padding: 8px 20px;
189
+ display: flex;
190
+ justify-content: space-between;
191
+ align-items: center;
192
+ font-size: 0.75rem;
193
+ color: var(--text-muted);
194
+ background: linear-gradient(to top, var(--bg-card) 60%, transparent);
195
+ z-index: 3;
196
+ pointer-events: none;
197
+ }
198
+
199
+ .editor-stats .active-emotions {
200
+ display: flex;
201
+ gap: 8px;
202
+ align-items: center;
203
+ }
204
+
205
+ .active-emotion-pill {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: 4px;
209
+ padding: 2px 8px;
210
+ border-radius: 10px;
211
+ font-size: 0.7rem;
212
+ font-weight: 500;
213
+ background: rgba(255,255,255,0.05);
214
+ backdrop-filter: blur(4px);
215
+ }
216
+
217
+ .active-emotion-dot {
218
+ width: 6px;
219
+ height: 6px;
220
+ border-radius: 50%;
221
  }
222
 
223
  #editorOverlay {
224
  position: absolute;
225
  top: 0; left: 0; right: 0; bottom: 0;
226
+ padding: 24px 24px 40px 24px;
227
+ font-size: 1.05rem;
228
+ line-height: 1.75;
229
  font-family: inherit;
230
  color: var(--text-primary);
231
  white-space: pre-wrap;
 
234
  overflow-y: auto;
235
  pointer-events: none;
236
  z-index: 1;
237
+ letter-spacing: 0.01em;
238
  }
239
 
240
  #editorOverlay .word {
241
  pointer-events: auto;
242
  cursor: pointer;
243
+ transition: all 0.25s ease;
244
+ padding: 1px 3px;
245
+ margin: 0 -3px;
246
+ border-radius: 4px;
247
+ position: relative;
248
+ }
249
+
250
+ #editorOverlay .word:hover {
251
+ filter: brightness(1.3);
252
+ transform: scale(1.02);
253
+ }
254
+
255
+ #editorOverlay .word.has-emotion {
256
+ text-shadow: 0 0 20px currentColor;
257
+ }
258
+
259
+ /* Underline decoration for emotional words */
260
+ #editorOverlay .word.has-emotion::after {
261
+ content: '';
262
+ position: absolute;
263
+ bottom: -1px;
264
+ left: 3px;
265
+ right: 3px;
266
+ height: 2px;
267
+ border-radius: 1px;
268
+ background: currentColor;
269
+ opacity: 0.4;
270
+ }
271
+
272
+ .placeholder-text {
273
+ color: var(--text-muted) !important;
274
+ font-style: italic;
275
+ font-weight: 300;
276
  }
277
 
278
  #editor {
279
  width: 100%;
280
  height: 100%;
281
  background: transparent;
282
+ color: transparent;
283
+ caret-color: #8b8bff;
284
  border: none;
285
+ padding: 24px 24px 40px 24px;
286
+ font-size: 1.05rem;
287
+ line-height: 1.75;
288
  font-family: inherit;
289
  resize: none;
290
  outline: none;
 
294
  white-space: pre-wrap;
295
  word-break: break-word;
296
  overflow-wrap: break-word;
297
+ letter-spacing: 0.01em;
298
+ }
299
+
300
+ /* Scrollbar styling */
301
+ #editor::-webkit-scrollbar,
302
+ #editorOverlay::-webkit-scrollbar {
303
+ width: 6px;
304
+ }
305
+
306
+ #editor::-webkit-scrollbar-track,
307
+ #editorOverlay::-webkit-scrollbar-track {
308
+ background: transparent;
309
+ }
310
+
311
+ #editor::-webkit-scrollbar-thumb,
312
+ #editorOverlay::-webkit-scrollbar-thumb {
313
+ background: var(--border);
314
+ border-radius: 3px;
315
+ }
316
+
317
+ #editor::-webkit-scrollbar-thumb:hover,
318
+ #editorOverlay::-webkit-scrollbar-thumb:hover {
319
+ background: #2a2a3a;
320
  }
321
 
322
  /* Tracks List */
323
  .tracks-container {
324
  display: flex;
325
+ gap: 8px;
326
  flex-wrap: wrap;
327
+ padding: 0 4px;
328
  min-height: 20px;
329
+ align-items: center;
330
+ }
331
+
332
+ .tracks-label {
333
+ font-size: 0.75rem;
334
+ color: var(--text-muted);
335
+ text-transform: uppercase;
336
+ letter-spacing: 0.05em;
337
+ font-weight: 600;
338
  }
339
 
340
  .track-item {
 
342
  align-items: center;
343
  gap: 6px;
344
  background: var(--bg-tertiary);
345
+ padding: 5px 12px;
346
+ border-radius: 20px;
347
+ font-size: 0.8rem;
348
  border: 1px solid var(--border);
349
+ cursor: pointer;
350
+ transition: all 0.2s;
351
+ font-weight: 500;
352
+ }
353
+
354
+ .track-item:hover {
355
+ background: #1e1e2e;
356
+ border-color: #2a2a3a;
357
+ }
358
+
359
+ .track-item.active {
360
+ border-color: var(--accent);
361
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.15);
362
  }
363
 
364
  .track-color {
365
+ width: 8px;
366
+ height: 8px;
367
  border-radius: 50%;
368
+ box-shadow: 0 0 6px currentColor;
369
+ }
370
+
371
+ .track-point-count {
372
+ font-size: 0.7rem;
373
+ color: var(--text-muted);
374
+ background: rgba(255,255,255,0.05);
375
+ padding: 1px 6px;
376
+ border-radius: 8px;
377
  }
378
 
379
  .track-delete {
380
  cursor: pointer;
381
+ color: var(--text-muted);
382
  font-weight: bold;
383
+ margin-left: 2px;
384
  transition: color 0.2s;
385
+ font-size: 0.9rem;
386
+ line-height: 1;
387
  }
388
 
389
  .track-delete:hover { color: #ef4444; }
 
391
  /* Timeline Section */
392
  .timeline-section {
393
  background: var(--bg-secondary);
394
+ border-radius: 14px;
395
  border: 1px solid var(--border);
396
+ padding: 16px 20px;
397
  flex-shrink: 0;
398
  }
399
 
 
401
  display: flex;
402
  justify-content: space-between;
403
  align-items: center;
404
+ margin-bottom: 12px;
405
  flex-wrap: wrap;
406
  gap: 10px;
407
  }
408
 
409
+ .timeline-title {
410
+ font-size: 0.95rem;
411
+ font-weight: 600;
412
+ color: var(--text-primary);
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 8px;
416
+ }
417
+
418
+ .timeline-title-icon {
419
+ font-size: 1rem;
420
+ opacity: 0.7;
421
+ }
422
 
423
+ .emotion-input-container {
424
+ display: flex;
425
+ gap: 8px;
426
+ align-items: center;
427
+ }
428
 
429
  #emotionInput {
430
+ padding: 8px 14px;
431
  background: var(--bg-tertiary);
432
  border: 1px solid var(--border);
433
+ border-radius: 8px;
434
  color: var(--text-primary);
435
+ font-size: 0.85rem;
436
  width: 200px;
437
+ font-family: inherit;
438
+ transition: border-color 0.2s;
439
+ }
440
+
441
+ #emotionInput:focus {
442
+ outline: none;
443
+ border-color: rgba(99, 102, 241, 0.4);
444
  }
445
 
446
  .timeline-canvas-container {
447
  position: relative;
448
  background: var(--bg-tertiary);
449
+ border-radius: 10px;
450
  overflow: hidden;
451
  cursor: crosshair;
452
+ border: 1px solid var(--border-subtle);
453
  }
454
 
455
+ #timelineCanvas {
456
+ display: block;
457
+ width: 100%;
458
+ }
459
+
460
+ /* Y-axis labels */
461
+ .timeline-y-labels {
462
+ position: absolute;
463
+ top: 0;
464
+ left: 0;
465
+ bottom: 0;
466
+ width: 30px;
467
+ display: flex;
468
+ flex-direction: column;
469
+ justify-content: space-between;
470
+ padding: 18px 0;
471
+ pointer-events: none;
472
+ z-index: 2;
473
+ }
474
+
475
+ .y-label {
476
+ font-size: 0.65rem;
477
+ color: var(--text-muted);
478
+ text-align: right;
479
+ padding-right: 6px;
480
+ }
481
 
482
  .instructions {
483
+ margin-top: 12px;
484
+ padding: 10px 14px;
485
  background: var(--bg-tertiary);
486
+ border-radius: 8px;
487
+ font-size: 0.75rem;
488
+ color: var(--text-muted);
489
+ line-height: 1.6;
490
+ border: 1px solid var(--border-subtle);
491
+ }
492
+
493
+ .instructions strong {
494
  color: var(--text-secondary);
 
495
  }
496
 
497
+ .instructions kbd {
498
+ display: inline-block;
499
+ padding: 1px 5px;
500
+ font-size: 0.7rem;
501
+ font-family: inherit;
502
+ background: rgba(255,255,255,0.06);
503
+ border: 1px solid var(--border);
504
+ border-radius: 4px;
505
+ color: var(--text-secondary);
506
+ }
507
 
508
  /* Modal */
509
  .modal {
510
  display: none;
511
  position: fixed;
512
  inset: 0;
513
+ background: rgba(0, 0, 0, 0.85);
514
  z-index: 1000;
515
  justify-content: center;
516
  align-items: center;
517
+ backdrop-filter: blur(8px);
518
  }
519
 
520
  .modal.active { display: flex; }
521
 
522
  .modal-content {
523
  background: var(--bg-secondary);
524
+ border-radius: 16px;
525
  padding: 30px;
526
+ max-width: 480px;
527
  width: 90%;
528
  border: 1px solid var(--border);
529
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
530
+ }
531
+
532
+ .modal-content h2 {
533
+ margin-bottom: 8px;
534
+ font-size: 1.2rem;
535
+ font-weight: 600;
536
+ }
537
+
538
+ .modal-subtitle {
539
+ color: var(--text-muted);
540
+ font-size: 0.82rem;
541
+ margin-bottom: 20px;
542
+ line-height: 1.5;
543
+ }
544
+
545
+ .form-group {
546
+ margin-bottom: 15px;
547
+ }
548
+
549
+ .form-group label {
550
+ display: block;
551
+ margin-bottom: 6px;
552
+ font-size: 0.82rem;
553
+ color: var(--text-secondary);
554
+ font-weight: 500;
555
+ }
556
+
557
+ .form-group input {
558
+ width: 100%;
559
+ padding: 10px 14px;
560
+ background: var(--bg-tertiary);
561
+ border: 1px solid var(--border);
562
+ border-radius: 8px;
563
+ color: var(--text-primary);
564
+ font-size: 0.9rem;
565
+ font-family: inherit;
566
+ transition: border-color 0.2s;
567
  }
568
 
569
+ .form-group input:focus {
570
+ outline: none;
571
+ border-color: rgba(99, 102, 241, 0.4);
572
+ }
573
 
574
+ .modal-actions {
575
+ display: flex;
576
+ gap: 10px;
577
+ justify-content: flex-end;
578
+ margin-top: 24px;
579
+ }
580
 
581
  /* Status */
582
  .status-bar {
583
  margin-top: 10px;
584
+ padding: 10px 14px;
585
  background: var(--bg-tertiary);
586
+ border-radius: 8px;
587
+ font-size: 0.82rem;
588
  color: var(--text-secondary);
589
  display: none;
590
+ border: 1px solid var(--border-subtle);
591
+ animation: statusFadeIn 0.3s ease;
592
+ }
593
+
594
+ @keyframes statusFadeIn {
595
+ from { opacity: 0; transform: translateY(5px); }
596
+ to { opacity: 1; transform: translateY(0); }
597
  }
598
+
599
  .status-bar.active { display: block; }
600
+ .status-bar.success { border-left: 3px solid #10b981; color: #6ee7b7; }
601
+ .status-bar.error { border-left: 3px solid #ef4444; color: #fca5a5; }
602
+
603
+ /* Particle canvas for emotional ambient effect */
604
+ #particleCanvas {
605
+ position: absolute;
606
+ top: 0; left: 0;
607
+ width: 100%;
608
+ height: 100%;
609
+ pointer-events: none;
610
+ z-index: 0;
611
+ opacity: 0.6;
612
+ }
613
+
614
+ /* Emotion intensity bar on editor side */
615
+ .emotion-sidebar {
616
+ position: absolute;
617
+ top: 0;
618
+ right: 0;
619
+ width: 4px;
620
+ height: 100%;
621
+ z-index: 3;
622
+ pointer-events: none;
623
+ border-radius: 0 16px 16px 0;
624
+ overflow: hidden;
625
+ }
626
+
627
+ .emotion-sidebar-fill {
628
+ position: absolute;
629
+ bottom: 0;
630
+ width: 100%;
631
+ transition: height 0.3s ease, background 0.3s ease;
632
+ border-radius: 2px;
633
+ }
634
  </style>
635
  </head>
636
  <body>
637
  <div class="container">
638
  <div class="header">
639
+ <div class="header-left">
640
+ <div class="header-icon">✦</div>
641
+ <h1>Self-Distillation</h1>
642
+ </div>
643
  <div class="header-controls">
644
+ <button class="btn btn-secondary" onclick="exportJSON()">
645
+ <span class="btn-icon"></span> Export
646
+ </button>
647
+ <button class="btn btn-danger" onclick="clearAll()">
648
+ <span class="btn-icon">✕</span> Clear
649
+ </button>
650
+ <button class="btn btn-primary" onclick="showHFModal()">
651
+ <span class="btn-icon">⬆</span> Push to HF
652
+ </button>
653
  </div>
654
  </div>
655
 
656
  <div class="editor-section">
657
  <div class="editor-wrapper" id="editorWrapper">
658
+ <div class="editor-ambient" id="editorAmbient"></div>
659
+ <canvas id="particleCanvas"></canvas>
660
  <div id="editorOverlay"></div>
661
+ <textarea id="editor" placeholder=""></textarea>
662
+ <div class="editor-stats" id="editorStats">
663
+ <span id="wordCount">0 words</span>
664
+ <div class="active-emotions" id="activeEmotionsDisplay"></div>
665
+ </div>
666
+ <div class="emotion-sidebar" id="emotionSidebar">
667
+ <div class="emotion-sidebar-fill" id="emotionSidebarFill"></div>
668
+ </div>
669
+ </div>
670
+ <div class="tracks-container" id="tracksList">
671
+ <span class="tracks-label">Tracks</span>
672
  </div>
 
673
  </div>
674
 
675
  <div class="timeline-section">
676
  <div class="timeline-header">
677
+ <div class="timeline-title">
678
+ <span class="timeline-title-icon">◆</span>
679
+ Emotional Timeline
680
+ </div>
681
  <div class="emotion-input-container">
682
+ <input type="text" id="emotionInput" placeholder="e.g., Joy, Anxiety, Wonder…">
683
+ <button class="btn btn-primary" onclick="addEmotionTrack()">+ Add Track</button>
684
  </div>
685
  </div>
686
+
687
  <div class="timeline-canvas-container">
688
  <canvas id="timelineCanvas"></canvas>
689
  </div>
690
 
691
  <div class="instructions">
692
+ <kbd>Click</kbd> timeline to add point ·
693
+ <kbd>Drag</kbd> vertically for intensity ·
694
+ <kbd>Drag</kbd> handles for fades ·
695
+ <kbd>Hover</kbd> points/words for details ·
696
+ <kbd>Double-click</kbd> to remove point ·
697
+ <kbd>Click</kbd> colored word to jump
698
  </div>
699
 
700
  <div class="status-bar" id="statusBar"></div>
 
704
  <div class="modal" id="hfModal">
705
  <div class="modal-content">
706
  <h2>Push to HuggingFace</h2>
707
+ <p class="modal-subtitle">Fetch existing data and append your new entry without overwriting.</p>
708
  <div class="form-group">
709
  <label>HF Token</label>
710
  <input type="password" id="hfToken" placeholder="hf_...">
 
731
  dragging: false,
732
  dragType: null,
733
  dragStart: { x: 0, y: 0 },
734
+ cursorWordIndex: 0,
735
+ particles: [],
736
+ selectedTrackId: null
737
  };
738
 
739
  // DOM Elements
740
  const editor = document.getElementById('editor');
741
  const editorOverlay = document.getElementById('editorOverlay');
742
  const editorWrapper = document.getElementById('editorWrapper');
743
+ const editorAmbient = document.getElementById('editorAmbient');
744
+ const particleCanvasEl = document.getElementById('particleCanvas');
745
+ const particleCtx = particleCanvasEl.getContext('2d');
746
  const canvas = document.getElementById('timelineCanvas');
747
  const ctx = canvas.getContext('2d');
748
  const emotionInput = document.getElementById('emotionInput');
749
  const statusBar = document.getElementById('statusBar');
750
  const tracksList = document.getElementById('tracksList');
751
+ const wordCountEl = document.getElementById('wordCount');
752
+ const activeEmotionsDisplay = document.getElementById('activeEmotionsDisplay');
753
+ const emotionSidebar = document.getElementById('emotionSidebar');
754
+ const emotionSidebarFill = document.getElementById('emotionSidebarFill');
755
+
756
+ const colorPalette = [
757
+ '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316',
758
+ '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#a855f7',
759
+ '#14b8a6', '#f472b6', '#fb923c', '#a3e635'
760
+ ];
761
  let colorIndex = 0;
762
 
763
+ // Tooltip
764
  const tooltip = document.createElement('div');
765
+ tooltip.style.cssText = `
766
+ display:none; position:fixed;
767
+ background: rgba(14,14,22,0.95);
768
+ color: #e8e8f0;
769
+ padding: 6px 12px;
770
+ border-radius: 8px;
771
+ font-size: 0.75rem;
772
+ pointer-events: none;
773
+ z-index: 100;
774
+ border: 1px solid #2a2a3a;
775
+ box-shadow: 0 8px 25px rgba(0,0,0,0.5);
776
+ backdrop-filter: blur(10px);
777
+ font-family: 'Inter', sans-serif;
778
+ max-width: 250px;
779
+ line-height: 1.4;
780
+ `;
781
  document.body.appendChild(tooltip);
782
 
783
+ function showTooltip(html, x, y) {
784
+ tooltip.innerHTML = html;
785
  tooltip.style.display = 'block';
786
+ const rect = tooltip.getBoundingClientRect();
787
+ let left = x + 15;
788
+ let top = y - 10;
789
+ if (left + rect.width > window.innerWidth - 10) left = x - rect.width - 15;
790
+ if (top + rect.height > window.innerHeight - 10) top = y - rect.height - 10;
791
+ tooltip.style.left = left + 'px';
792
+ tooltip.style.top = top + 'px';
793
  }
794
  function hideTooltip() { tooltip.style.display = 'none'; }
795
 
796
+ // Particles
797
+ function initParticleCanvas() {
798
+ const rect = editorWrapper.getBoundingClientRect();
799
+ particleCanvasEl.width = rect.width;
800
+ particleCanvasEl.height = rect.height;
801
+ }
802
+
803
+ function spawnParticles(color, intensity) {
804
+ const count = Math.floor(intensity * 3);
805
+ for (let i = 0; i < count; i++) {
806
+ state.particles.push({
807
+ x: Math.random() * particleCanvasEl.width,
808
+ y: Math.random() * particleCanvasEl.height,
809
+ vx: (Math.random() - 0.5) * 0.3,
810
+ vy: -Math.random() * 0.5 - 0.2,
811
+ life: 1,
812
+ decay: 0.003 + Math.random() * 0.005,
813
+ size: Math.random() * 3 + 1,
814
+ color: color
815
+ });
816
+ }
817
+ }
818
+
819
+ function updateParticles() {
820
+ particleCtx.clearRect(0, 0, particleCanvasEl.width, particleCanvasEl.height);
821
+
822
+ state.particles = state.particles.filter(p => p.life > 0);
823
+
824
+ state.particles.forEach(p => {
825
+ p.x += p.vx;
826
+ p.y += p.vy;
827
+ p.life -= p.decay;
828
+
829
+ const rgb = hexToRgb(p.color);
830
+ particleCtx.beginPath();
831
+ particleCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
832
+ particleCtx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${p.life * 0.4})`;
833
+ particleCtx.fill();
834
+ });
835
+
836
+ requestAnimationFrame(updateParticles);
837
+ }
838
+
839
  // Initialization
840
  function init() {
841
  loadFromLocalStorage();
842
  setupEventListeners();
843
  resizeCanvas();
844
+ initParticleCanvas();
845
  renderTracksList();
846
  updateOverlay();
847
  render();
848
+ updateParticles();
849
  }
850
 
851
  function setupEventListeners() {
 
856
  editorOverlay.scrollTop = editor.scrollTop;
857
  editorOverlay.scrollLeft = editor.scrollLeft;
858
  });
859
+
860
  canvas.addEventListener('mousedown', handleCanvasMouseDown);
861
  canvas.addEventListener('mousemove', handleCanvasMouseMove);
862
  canvas.addEventListener('mouseup', handleCanvasMouseUp);
863
  canvas.addEventListener('mouseleave', () => { handleCanvasMouseUp(); hideTooltip(); });
864
  canvas.addEventListener('dblclick', handleCanvasDoubleClick);
865
+
866
+ emotionInput.addEventListener('keydown', (e) => {
867
+ if (e.key === 'Enter') addEmotionTrack();
868
+ });
869
+
870
+ window.addEventListener('resize', () => {
871
+ resizeCanvas();
872
+ initParticleCanvas();
873
+ render();
874
+ });
875
  }
876
 
877
  // Text Handling
 
888
  function handleTextChange(e) {
889
  state.text = e.target.value;
890
  state.words = getWords(state.text);
 
891
  state.tracks.forEach(track => {
892
  track.points = track.points.filter(p => p.wordIndex < state.words.length);
893
  });
 
898
 
899
  function handleEditorClick() {
900
  const charIndex = editor.selectionStart;
901
+ let wordIdx = state.words.length;
902
  for (let i = 0; i < state.words.length; i++) {
903
  if (charIndex <= state.words[i].end) {
904
  wordIdx = i;
 
906
  }
907
  }
908
  state.cursorWordIndex = wordIdx;
909
+ updateEditorEffects();
910
  render();
911
  }
912
 
913
  function updateOverlay() {
914
  let html = '';
915
  let lastIndex = 0;
916
+
917
+ if (state.words.length === 0) {
918
+ editorOverlay.innerHTML = '<span class="placeholder-text">Begin writing your thoughts here… emotions will paint the words as you map them.</span>';
919
+ wordCountEl.textContent = '0 words';
920
+ return;
921
+ }
922
+
923
  state.words.forEach((word, i) => {
924
+ const space = state.text.substring(lastIndex, word.start)
925
+ .replace(/ /g, '&nbsp;')
926
+ .replace(/\n/g, '<br>')
927
+ .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
928
  html += space;
929
+
 
930
  const emotionsAtWord = getActiveEmotionsAtWord(i);
931
+ let bgStyle = '';
932
+ let textColorStyle = '';
933
  let titleText = '';
934
+ let hasEmotion = false;
935
+
936
  if (emotionsAtWord.length > 0) {
937
+ let r = 0, g = 0, b = 0, total = 0;
938
  emotionsAtWord.forEach(e => {
939
  const rgb = hexToRgb(e.color);
940
  r += rgb.r * e.intensity;
 
942
  b += rgb.b * e.intensity;
943
  total += e.intensity;
944
  });
945
+ if (total > 0) {
946
+ r = Math.round(r / total);
947
+ g = Math.round(g / total);
948
+ b = Math.round(b / total);
949
  const maxInt = Math.max(...emotionsAtWord.map(e => e.intensity));
950
+ const alpha = Math.max(0.08, maxInt * 0.35);
951
+ bgStyle = `background: linear-gradient(135deg, rgba(${r},${g},${b},${alpha}), rgba(${r},${g},${b},${alpha * 0.5}));`;
952
+ // Tint text color slightly toward emotion
953
+ const textBlend = Math.min(0.6, maxInt * 0.5);
954
+ const tr = Math.round(232 * (1 - textBlend) + r * textBlend);
955
+ const tg = Math.round(232 * (1 - textBlend) + g * textBlend);
956
+ const tb = Math.round(240 * (1 - textBlend) + b * textBlend);
957
+ textColorStyle = `color: rgb(${tr},${tg},${tb});`;
958
+ titleText = emotionsAtWord.map(e => `${e.label}: ${(e.intensity * 100).toFixed(0)}%`).join(', ');
959
+ hasEmotion = true;
960
  }
961
  }
962
+
963
  const escapedText = word.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
964
+ const emotionClass = hasEmotion ? ' has-emotion' : '';
965
+ html += `<span class="word${emotionClass}" data-index="${i}" style="${bgStyle}${textColorStyle}" title="${titleText}">${escapedText}</span>`;
966
  lastIndex = word.end;
967
  });
968
+
969
+ const trailing = state.text.substring(lastIndex)
970
+ .replace(/ /g, '&nbsp;')
971
+ .replace(/\n/g, '<br>')
972
+ .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
973
  html += trailing;
974
+
975
+ editorOverlay.innerHTML = html;
976
+ wordCountEl.textContent = `${state.words.length} word${state.words.length !== 1 ? 's' : ''}`;
977
+
978
  document.querySelectorAll('#editorOverlay .word').forEach(span => {
979
  span.addEventListener('click', (e) => {
980
  const wordIndex = parseInt(span.getAttribute('data-index'));
 
984
  handleEditorClick();
985
  e.stopPropagation();
986
  });
987
+
988
  span.addEventListener('mousemove', (e) => {
989
  const wordIndex = parseInt(span.getAttribute('data-index'));
990
  const emotions = getActiveEmotionsAtWord(wordIndex);
991
+ if (emotions.length > 0) {
992
+ const word = state.words[wordIndex];
993
+ let html = `<div style="margin-bottom:3px;font-weight:600;color:#fff;">"${word.text}"</div>`;
994
+ emotions.forEach(em => {
995
+ const pct = (em.intensity * 100).toFixed(0);
996
+ html += `<div style="display:flex;align-items:center;gap:6px;margin-top:3px;">
997
+ <span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:${em.color};box-shadow:0 0 4px ${em.color};"></span>
998
+ <span>${em.label}</span>
999
+ <span style="color:var(--text-muted);margin-left:auto;">${pct}%</span>
1000
+ </div>`;
1001
+ });
1002
+ showTooltip(html, e.clientX, e.clientY);
1003
+ } else {
1004
+ hideTooltip();
1005
+ }
1006
  });
1007
  span.addEventListener('mouseleave', hideTooltip);
1008
  });
1009
  }
1010
 
1011
+ // Canvas Setup
1012
  function resizeCanvas() {
1013
  const container = canvas.parentElement;
1014
  canvas.width = container.clientWidth;
1015
+ canvas.height = 180;
1016
  }
1017
 
1018
  function wordIndexToX(wordIndex) {
1019
  const totalWords = state.words.length || 1;
1020
+ const padding = 30, availableWidth = canvas.width - (padding * 2);
1021
  return padding + (wordIndex / totalWords) * availableWidth;
1022
  }
1023
 
1024
  function xToWordIndex(x) {
1025
+ const padding = 30, availableWidth = canvas.width - (padding * 2);
1026
  const totalWords = state.words.length || 1;
1027
  const normalizedX = Math.max(0, Math.min(x - padding, availableWidth));
1028
  return Math.round((normalizedX / availableWidth) * totalWords);
1029
  }
1030
 
1031
  function intensityToY(intensity) {
1032
+ const padding = 25, availableHeight = canvas.height - (padding * 2);
1033
  return padding + availableHeight - (intensity * availableHeight);
1034
  }
1035
 
1036
  function yToIntensity(y) {
1037
+ const padding = 25, availableHeight = canvas.height - (padding * 2);
1038
  const normalizedY = Math.max(padding, Math.min(y, canvas.height - padding));
1039
  return 1 - ((normalizedY - padding) / availableHeight);
1040
  }
 
1048
  function handleCanvasMouseDown(e) {
1049
  const coords = getCanvasCoordinates(e);
1050
  const clickedItem = findItemAtPosition(coords.x, coords.y);
1051
+
1052
  if (clickedItem) {
1053
  state.selectedPoint = clickedItem;
1054
  state.dragging = true;
1055
  state.dragType = clickedItem.type;
1056
  state.dragStart = { x: coords.x, y: coords.y };
1057
  } else {
1058
+ // Use selected track or last track
1059
+ let activeTrack = state.tracks.find(t => t.id === state.selectedTrackId) || state.tracks[state.tracks.length - 1];
1060
  if (activeTrack) {
1061
  const wordIndex = xToWordIndex(coords.x);
1062
+ const newPoint = {
1063
+ id: Date.now(),
1064
+ wordIndex: wordIndex,
1065
+ intensity: Math.max(0, Math.min(1, yToIntensity(coords.y))),
1066
+ fadeIn: 0,
1067
+ fadeOut: 0
1068
+ };
1069
  activeTrack.points.push(newPoint);
1070
  activeTrack.points.sort((a, b) => a.wordIndex - b.wordIndex);
1071
  state.selectedPoint = { type: 'point', point: newPoint, track: activeTrack };
1072
+ state.dragging = true;
1073
+ state.dragType = 'point';
1074
+ render();
1075
+ updateOverlay();
1076
+ saveToLocalStorage();
1077
  }
1078
  }
1079
  }
 
1083
  if (!state.dragging) {
1084
  const item = findItemAtPosition(coords.x, coords.y);
1085
  if (item && item.type === 'point') {
1086
+ canvas.style.cursor = 'grab';
1087
+ const pct = (item.point.intensity * 100).toFixed(0);
1088
+ const wordText = state.words[item.point.wordIndex]?.text || '';
1089
+ showTooltip(`
1090
+ <div style="font-weight:600;color:${item.track.color};">${item.track.label}</div>
1091
+ <div style="margin-top:3px;">Intensity: ${pct}%</div>
1092
+ ${wordText ? `<div style="color:var(--text-muted);">Word: "${wordText}"</div>` : ''}
1093
+ `, e.clientX, e.clientY);
1094
+ } else if (item) {
1095
+ canvas.style.cursor = 'ew-resize';
1096
+ showTooltip(`Drag to adjust ${item.type === 'fadeIn' ? 'fade in' : 'fade out'}`, e.clientX, e.clientY);
1097
  } else {
1098
  canvas.style.cursor = 'crosshair';
1099
  hideTooltip();
 
1102
  }
1103
 
1104
  if (!state.selectedPoint) return;
1105
+ canvas.style.cursor = 'grabbing';
1106
  const deltaX = coords.x - state.dragStart.x;
1107
 
1108
  if (state.dragType === 'point') {
 
1118
  }
1119
 
1120
  state.dragStart = { x: coords.x, y: coords.y };
1121
+ render();
1122
+ updateOverlay();
1123
+ saveToLocalStorage();
1124
  }
1125
 
1126
+ function handleCanvasMouseUp() {
1127
+ state.dragging = false;
1128
+ state.dragType = null;
1129
+ canvas.style.cursor = 'crosshair';
1130
+ }
1131
 
1132
  function handleCanvasDoubleClick(e) {
1133
  const coords = getCanvasCoordinates(e);
1134
  const clickedItem = findItemAtPosition(coords.x, coords.y);
1135
  if (clickedItem && clickedItem.type === 'point') {
1136
  clickedItem.track.points = clickedItem.track.points.filter(p => p.id !== clickedItem.point.id);
1137
+ render();
1138
+ updateOverlay();
1139
+ saveToLocalStorage();
1140
+ showStatus('Point removed', 'success');
1141
  }
1142
  }
1143
 
 
1168
  }
1169
 
1170
  function drawGrid() {
1171
+ const padding = 25;
1172
+
1173
+ // Horizontal intensity lines
 
 
 
1174
  for (let i = 0; i <= 1; i += 0.25) {
1175
  const y = intensityToY(i);
1176
+ ctx.strokeStyle = i === 0 ? '#252535' : '#1a1a28';
1177
+ ctx.lineWidth = 1;
1178
+ ctx.beginPath();
1179
+ ctx.moveTo(30, y);
1180
+ ctx.lineTo(canvas.width - 10, y);
1181
+ ctx.stroke();
1182
+
1183
+ // Labels
1184
+ ctx.fillStyle = '#33334a';
1185
+ ctx.font = '10px Inter, sans-serif';
1186
+ ctx.textAlign = 'right';
1187
+ ctx.fillText((i * 100).toFixed(0) + '%', 26, y + 3);
1188
+ }
1189
+
1190
+ // Vertical word markers (subtle)
1191
+ if (state.words.length > 0) {
1192
+ const step = Math.max(1, Math.floor(state.words.length / 25));
1193
+ for (let i = 0; i <= state.words.length; i += step) {
1194
+ const x = wordIndexToX(i);
1195
+ ctx.strokeStyle = '#14141f';
1196
+ ctx.lineWidth = 1;
1197
+ ctx.beginPath();
1198
+ ctx.moveTo(x, padding);
1199
+ ctx.lineTo(x, canvas.height - padding);
1200
+ ctx.stroke();
1201
+ }
1202
  }
1203
  }
1204
 
1205
  function drawTrack(track) {
1206
  if (track.points.length === 0) return;
1207
+
1208
+ const rgb = hexToRgb(track.color);
1209
+ const isSelected = track.id === state.selectedTrackId;
1210
+
1211
+ // Draw filled area
1212
+ ctx.beginPath();
1213
+ ctx.moveTo(wordIndexToX(track.points[0].wordIndex), canvas.height - 25);
1214
+
1215
+ // Generate smooth curve points
1216
+ track.points.forEach((point, index) => {
1217
+ const x = wordIndexToX(point.wordIndex);
1218
+ const y = intensityToY(point.intensity);
1219
+
1220
+ if (index === 0) {
1221
+ ctx.lineTo(x, y);
1222
+ } else {
1223
+ const prevX = wordIndexToX(track.points[index - 1].wordIndex);
1224
+ const prevY = intensityToY(track.points[index - 1].intensity);
1225
+ const cpx = (prevX + x) / 2;
1226
+ ctx.bezierCurveTo(cpx, prevY, cpx, y, x, y);
1227
+ }
1228
+ });
1229
+
1230
+ ctx.lineTo(wordIndexToX(track.points[track.points.length - 1].wordIndex), canvas.height - 25);
1231
+ ctx.closePath();
1232
+
1233
+ // Gradient fill
1234
+ const gradient = ctx.createLinearGradient(0, 25, 0, canvas.height - 25);
1235
+ gradient.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${isSelected ? 0.2 : 0.1})`);
1236
+ gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
1237
+ ctx.fillStyle = gradient;
1238
+ ctx.fill();
1239
+
1240
+ // Draw line
1241
  ctx.beginPath();
1242
  track.points.forEach((point, index) => {
1243
+ const x = wordIndexToX(point.wordIndex);
1244
+ const y = intensityToY(point.intensity);
1245
+
1246
+ if (index === 0) {
1247
+ ctx.moveTo(x, y);
1248
+ } else {
1249
+ const prevX = wordIndexToX(track.points[index - 1].wordIndex);
1250
+ const prevY = intensityToY(track.points[index - 1].intensity);
1251
+ const cpx = (prevX + x) / 2;
1252
+ ctx.bezierCurveTo(cpx, prevY, cpx, y, x, y);
1253
+ }
1254
  });
1255
+
1256
+ ctx.strokeStyle = track.color;
1257
+ ctx.lineWidth = isSelected ? 3 : 2;
1258
+ ctx.shadowColor = track.color;
1259
+ ctx.shadowBlur = isSelected ? 12 : 6;
1260
  ctx.stroke();
1261
+ ctx.shadowBlur = 0;
1262
+
1263
+ // Draw points
 
 
 
1264
  track.points.forEach(point => drawPoint(point, track));
1265
  }
1266
 
1267
  function drawPoint(point, track) {
1268
+ const x = wordIndexToX(point.wordIndex);
1269
+ const y = intensityToY(point.intensity);
1270
+ const rgb = hexToRgb(track.color);
1271
+
1272
+ // Glow
1273
+ ctx.beginPath();
1274
+ ctx.arc(x, y, 12, 0, Math.PI * 2);
1275
+ ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15)`;
1276
+ ctx.fill();
1277
+
1278
+ // Outer ring
1279
+ ctx.beginPath();
1280
+ ctx.arc(x, y, 6, 0, Math.PI * 2);
1281
+ ctx.fillStyle = track.color;
1282
+ ctx.fill();
1283
+
1284
+ // Inner dot
1285
+ ctx.beginPath();
1286
+ ctx.arc(x, y, 3, 0, Math.PI * 2);
1287
+ ctx.fillStyle = '#fff';
1288
+ ctx.fill();
1289
+
1290
+ // Fade handles
1291
  if (point.fadeIn > 0) {
1292
  const fadeInX = wordIndexToX(point.wordIndex - point.fadeIn);
1293
+ ctx.beginPath();
1294
+ ctx.arc(fadeInX, y, 4, 0, Math.PI * 2);
1295
+ ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.6)`;
1296
+ ctx.fill();
1297
+ ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`;
1298
+ ctx.lineWidth = 1;
1299
+ ctx.setLineDash([4, 4]);
1300
+ ctx.beginPath();
1301
+ ctx.moveTo(fadeInX, y);
1302
+ ctx.lineTo(x, y);
1303
+ ctx.stroke();
1304
+ ctx.setLineDash([]);
1305
  }
1306
  if (point.fadeOut > 0) {
1307
  const fadeOutX = wordIndexToX(point.wordIndex + point.fadeOut);
1308
+ ctx.beginPath();
1309
+ ctx.arc(fadeOutX, y, 4, 0, Math.PI * 2);
1310
+ ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.6)`;
1311
+ ctx.fill();
1312
+ ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`;
1313
+ ctx.lineWidth = 1;
1314
+ ctx.setLineDash([4, 4]);
1315
+ ctx.beginPath();
1316
+ ctx.moveTo(x, y);
1317
+ ctx.lineTo(fadeOutX, y);
1318
+ ctx.stroke();
1319
+ ctx.setLineDash([]);
1320
  }
1321
  }
1322
 
1323
  function drawCursorIndicator() {
1324
  if (state.words.length === 0) return;
1325
  const x = wordIndexToX(state.cursorWordIndex);
1326
+
1327
+ // Subtle glow line
1328
+ const gradient = ctx.createLinearGradient(x, 0, x, canvas.height);
1329
+ gradient.addColorStop(0, 'rgba(255,255,255,0)');
1330
+ gradient.addColorStop(0.3, 'rgba(255,255,255,0.15)');
1331
+ gradient.addColorStop(0.7, 'rgba(255,255,255,0.15)');
1332
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
1333
+
1334
+ ctx.strokeStyle = gradient;
1335
+ ctx.lineWidth = 1.5;
1336
+ ctx.beginPath();
1337
+ ctx.moveTo(x, 0);
1338
+ ctx.lineTo(x, canvas.height);
1339
+ ctx.stroke();
1340
+
1341
+ // Small diamond indicator at top
1342
+ ctx.fillStyle = 'rgba(255,255,255,0.8)';
1343
+ ctx.beginPath();
1344
+ ctx.moveTo(x, 8);
1345
+ ctx.lineTo(x + 4, 12);
1346
+ ctx.lineTo(x, 16);
1347
+ ctx.lineTo(x - 4, 12);
1348
+ ctx.closePath();
1349
+ ctx.fill();
1350
  }
1351
 
1352
+ function updateEditorEffects() {
1353
  const activeEmotions = getActiveEmotionsAtWord(state.cursorWordIndex);
1354
+
1355
+ // Update ambient glow
1356
+ if (activeEmotions.length === 0) {
1357
+ editorWrapper.style.boxShadow = 'none';
1358
+ editorWrapper.classList.remove('has-emotion');
1359
+ editorAmbient.style.background = 'none';
1360
+ emotionSidebarFill.style.height = '0%';
1361
+ activeEmotionsDisplay.innerHTML = '';
1362
+ return;
1363
+ }
1364
+
1365
+ editorWrapper.classList.add('has-emotion');
1366
+
1367
+ let r = 0, g = 0, b = 0, total = 0;
1368
  activeEmotions.forEach(e => {
1369
  const rgb = hexToRgb(e.color);
1370
+ r += rgb.r * e.intensity;
1371
+ g += rgb.g * e.intensity;
1372
+ b += rgb.b * e.intensity;
1373
+ total += e.intensity;
1374
  });
1375
+
1376
+ if (total > 0) {
1377
+ r = Math.round(r / total);
1378
+ g = Math.round(g / total);
1379
+ b = Math.round(b / total);
1380
  const avgInt = total / activeEmotions.length;
1381
+
1382
+ editorWrapper.style.boxShadow = `
1383
+ inset 0 0 80px rgba(${r}, ${g}, ${b}, ${avgInt * 0.08}),
1384
+ 0 0 30px rgba(${r}, ${g}, ${b}, ${avgInt * 0.15})
1385
+ `;
1386
+ editorWrapper.style.borderColor = `rgba(${r}, ${g}, ${b}, ${Math.max(0.2, avgInt * 0.4)})`;
1387
+
1388
+ editorAmbient.style.background = `radial-gradient(ellipse at 50% 50%, rgba(${r},${g},${b},${avgInt * 0.1}), transparent 70%)`;
1389
+
1390
+ // Sidebar fill
1391
+ emotionSidebarFill.style.height = `${avgInt * 100}%`;
1392
+ emotionSidebarFill.style.background = `linear-gradient(to top, rgba(${r},${g},${b},0.6), rgba(${r},${g},${b},0.2))`;
1393
+ emotionSidebarFill.style.boxShadow = `0 0 8px rgba(${r},${g},${b},0.4)`;
1394
+
1395
+ // Spawn particles
1396
+ activeEmotions.forEach(e => spawnParticles(e.color, e.intensity * 0.3));
1397
+
1398
+ // Active emotions display
1399
+ activeEmotionsDisplay.innerHTML = activeEmotions.map(e => `
1400
+ <div class="active-emotion-pill">
1401
+ <span class="active-emotion-dot" style="background:${e.color};box-shadow:0 0 4px ${e.color};"></span>
1402
+ ${e.label} ${(e.intensity * 100).toFixed(0)}%
1403
+ </div>
1404
+ `).join('');
1405
  }
1406
  }
1407
 
 
1409
  const activeEmotions = [];
1410
  state.tracks.forEach(track => {
1411
  track.points.forEach(point => {
1412
+ const start = point.wordIndex - point.fadeIn;
1413
+ const end = point.wordIndex + point.fadeOut;
1414
  if (wordIndex >= start && wordIndex <= end) {
1415
  let intensity = point.intensity;
1416
+ if (wordIndex < point.wordIndex && point.fadeIn > 0)
1417
+ intensity *= (wordIndex - start) / point.fadeIn;
1418
+ if (wordIndex > point.wordIndex && point.fadeOut > 0)
1419
+ intensity *= 1 - ((wordIndex - point.wordIndex) / point.fadeOut);
1420
+ activeEmotions.push({
1421
+ label: track.label,
1422
+ color: track.color,
1423
+ intensity: Math.max(0, Math.min(1, intensity))
1424
+ });
1425
  }
1426
  });
1427
  });
1428
  return activeEmotions;
1429
  }
1430
 
1431
+ // Track Management
1432
  function addEmotionTrack() {
1433
  const emotionName = emotionInput.value.trim();
1434
  if (!emotionName) { showStatus('Please enter an emotion name', 'error'); return; }
1435
+ const newTrack = {
1436
+ id: Date.now(),
1437
+ label: emotionName,
1438
+ color: colorPalette[colorIndex++ % colorPalette.length],
1439
+ points: []
1440
+ };
1441
+ state.tracks.push(newTrack);
1442
+ state.selectedTrackId = newTrack.id;
1443
  emotionInput.value = '';
1444
+ renderTracksList();
1445
+ render();
1446
+ saveToLocalStorage();
1447
+ showStatus(`Track "${emotionName}" added — click timeline to place points`, 'success');
1448
  }
1449
 
1450
  function renderTracksList() {
1451
+ tracksList.innerHTML = '<span class="tracks-label">Tracks</span>';
1452
+ if (state.tracks.length === 0) {
1453
+ tracksList.innerHTML += '<span style="font-size:0.8rem;color:var(--text-muted);font-style:italic;">No tracks yet</span>';
1454
+ return;
1455
+ }
1456
  state.tracks.forEach(track => {
1457
+ const isActive = track.id === state.selectedTrackId;
1458
  const item = document.createElement('div');
1459
+ item.className = 'track-item' + (isActive ? ' active' : '');
1460
+ item.innerHTML = `
1461
+ <div class="track-color" style="background:${track.color};color:${track.color}"></div>
1462
+ <span>${track.label}</span>
1463
+ <span class="track-point-count">${track.points.length}pt</span>
1464
+ <span class="track-delete" data-id="${track.id}">&times;</span>
1465
+ `;
1466
+ item.addEventListener('click', (e) => {
1467
+ if (e.target.classList.contains('track-delete')) return;
1468
+ state.selectedTrackId = track.id;
1469
+ renderTracksList();
1470
+ render();
1471
+ });
1472
+ item.querySelector('.track-delete').addEventListener('click', (e) => {
1473
+ e.stopPropagation();
1474
+ deleteTrack(track.id);
1475
+ });
1476
  tracksList.appendChild(item);
1477
  });
1478
  }
1479
 
1480
  function deleteTrack(trackId) {
1481
  state.tracks = state.tracks.filter(t => t.id !== trackId);
1482
+ if (state.selectedTrackId === trackId) {
1483
+ state.selectedTrackId = state.tracks.length > 0 ? state.tracks[state.tracks.length - 1].id : null;
1484
+ }
1485
+ renderTracksList();
1486
+ render();
1487
+ updateOverlay();
1488
+ saveToLocalStorage();
1489
  }
1490
 
1491
+ // Data Export
1492
  function generateDataset() {
1493
  const emotionalFlow = [];
1494
  state.tracks.forEach(track => {
 
1508
  const jsonStr = JSON.stringify(generateDataset(), null, 2);
1509
  const blob = new Blob([jsonStr], { type: 'application/json' });
1510
  const a = document.createElement('a');
1511
+ a.href = URL.createObjectURL(blob);
1512
+ a.download = 'self-distillation-dataset.json';
1513
+ a.click();
1514
  showStatus('Dataset exported successfully', 'success');
1515
  }
1516
 
1517
  function clearAll() {
1518
+ if (confirm('Clear all text and emotion tracks?')) {
1519
  localStorage.removeItem('self_distillation_data');
1520
+ state.text = '';
1521
+ state.tracks = [];
1522
+ state.words = [];
1523
+ state.cursorWordIndex = 0;
1524
+ state.selectedTrackId = null;
1525
+ state.particles = [];
1526
+ editor.value = '';
1527
+ editorWrapper.style.boxShadow = 'none';
1528
+ editorWrapper.style.borderColor = '';
1529
+ editorWrapper.classList.remove('has-emotion');
1530
+ editorAmbient.style.background = 'none';
1531
+ emotionSidebarFill.style.height = '0%';
1532
+ activeEmotionsDisplay.innerHTML = '';
1533
+ updateOverlay();
1534
+ renderTracksList();
1535
+ render();
1536
+ showStatus('All data cleared', 'success');
1537
  }
1538
  }
1539
 
1540
+ // HuggingFace
1541
  function showHFModal() {
1542
  const savedToken = localStorage.getItem('hf_token');
1543
  const savedRepo = localStorage.getItem('hf_repo_id');
 
1552
  const token = document.getElementById('hfToken').value.trim();
1553
  const repoId = document.getElementById('hfRepoId').value.trim();
1554
  if (!token || !repoId) { showStatus('Please provide both token and repository ID', 'error'); return; }
1555
+
1556
  localStorage.setItem('hf_token', token);
1557
  localStorage.setItem('hf_repo_id', repoId);
1558
+
1559
  try {
1560
+ showStatus('Fetching existing dataset', 'success');
1561
  let existingData = [];
1562
  try {
1563
  const res = await fetch(`https://huggingface.co/datasets/${repoId}/resolve/main/data.json`);
 
1566
  existingData = JSON.parse(text);
1567
  if (!Array.isArray(existingData)) existingData = [existingData];
1568
  }
1569
+ } catch (e) { console.log('No existing file, creating new.'); }
1570
 
1571
  existingData.push(generateDataset());
1572
 
1573
+ showStatus('Uploading to HuggingFace', 'success');
1574
  const commitUrl = `https://huggingface.co/api/datasets/${repoId}/commit/main`;
1575
  const response = await fetch(commitUrl, {
1576
  method: "POST",
 
1580
  operations: [{ key: "data.json", operation: "addOrUpdate", content: JSON.stringify(existingData, null, 2) }]
1581
  })
1582
  });
1583
+
1584
  if (!response.ok) throw new Error('Failed to commit to HF');
1585
  showStatus('Successfully appended to HuggingFace!', 'success');
1586
  closeHFModal();
 
1589
  }
1590
  }
1591
 
1592
+ // Storage & Utils
1593
  function saveToLocalStorage() {
1594
+ localStorage.setItem('self_distillation_data', JSON.stringify({
1595
+ text: state.text,
1596
+ tracks: state.tracks,
1597
+ colorIndex,
1598
+ selectedTrackId: state.selectedTrackId
1599
+ }));
1600
  }
1601
 
1602
  function loadFromLocalStorage() {
 
1604
  if (saved) {
1605
  try {
1606
  const data = JSON.parse(saved);
1607
+ state.text = data.text || '';
1608
+ state.tracks = data.tracks || [];
1609
+ colorIndex = data.colorIndex || 0;
1610
+ state.selectedTrackId = data.selectedTrackId || null;
1611
+ state.words = getWords(state.text);
1612
+ editor.value = state.text;
1613
+ } catch (e) { console.error('Failed to load:', e); }
1614
  }
1615
  }
1616
 
1617
  function hexToRgb(hex) {
1618
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1619
+ return result ? {
1620
+ r: parseInt(result[1], 16),
1621
+ g: parseInt(result[2], 16),
1622
+ b: parseInt(result[3], 16)
1623
+ } : { r: 0, g: 0, b: 0 };
1624
  }
1625
 
1626
  function showStatus(message, type) {
1627
+ statusBar.textContent = message;
1628
+ statusBar.className = `status-bar active ${type}`;
1629
  setTimeout(() => statusBar.classList.remove('active'), 3000);
1630
  }
1631