Update index.html
Browse files- 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: #
|
| 16 |
-
--bg-secondary: #
|
| 17 |
-
--bg-tertiary: #
|
| 18 |
-
--
|
| 19 |
-
--text-
|
|
|
|
|
|
|
| 20 |
--accent: #6366f1;
|
| 21 |
-
--
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
body {
|
|
@@ -33,8 +39,8 @@
|
|
| 33 |
display: flex;
|
| 34 |
flex-direction: column;
|
| 35 |
height: 100vh;
|
| 36 |
-
padding:
|
| 37 |
-
gap:
|
| 38 |
}
|
| 39 |
|
| 40 |
/* Header */
|
|
@@ -42,41 +48,94 @@
|
|
| 42 |
display: flex;
|
| 43 |
justify-content: space-between;
|
| 44 |
align-items: center;
|
| 45 |
-
padding:
|
| 46 |
background: var(--bg-secondary);
|
| 47 |
-
border-radius:
|
| 48 |
border: 1px solid var(--border);
|
| 49 |
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
.header h1 {
|
| 53 |
-
font-size: 1.
|
| 54 |
font-weight: 600;
|
| 55 |
-
background: linear-gradient(135deg, #
|
| 56 |
-webkit-background-clip: text;
|
| 57 |
-webkit-text-fill-color: transparent;
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
.header-controls {
|
| 61 |
display: flex;
|
| 62 |
-
gap:
|
| 63 |
}
|
| 64 |
|
| 65 |
.btn {
|
| 66 |
padding: 8px 16px;
|
| 67 |
border: none;
|
| 68 |
-
border-radius:
|
| 69 |
cursor: pointer;
|
| 70 |
-
font-size: 0.
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
-
.btn-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
.btn-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 94 |
border: 1px solid var(--border);
|
| 95 |
-
background: var(--bg-
|
| 96 |
overflow: hidden;
|
| 97 |
-
transition: box-shadow 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
#editorOverlay {
|
| 101 |
position: absolute;
|
| 102 |
top: 0; left: 0; right: 0; bottom: 0;
|
| 103 |
-
padding:
|
| 104 |
-
font-size: 1.
|
| 105 |
-
line-height: 1.
|
| 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:
|
| 120 |
-
padding:
|
| 121 |
-
margin: 0 -
|
| 122 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
#editor {
|
| 126 |
width: 100%;
|
| 127 |
height: 100%;
|
| 128 |
background: transparent;
|
| 129 |
-
color: transparent;
|
| 130 |
-
caret-color:
|
| 131 |
border: none;
|
| 132 |
-
padding:
|
| 133 |
-
font-size: 1.
|
| 134 |
-
line-height: 1.
|
| 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:
|
| 150 |
flex-wrap: wrap;
|
| 151 |
-
padding: 0
|
| 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:
|
| 161 |
-
border-radius:
|
| 162 |
-
font-size: 0.
|
| 163 |
border: 1px solid var(--border);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
.track-color {
|
| 167 |
-
width:
|
| 168 |
-
height:
|
| 169 |
border-radius: 50%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
}
|
| 171 |
|
| 172 |
.track-delete {
|
| 173 |
cursor: pointer;
|
| 174 |
-
color: var(--text-
|
| 175 |
font-weight: bold;
|
| 176 |
-
margin-left:
|
| 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:
|
| 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:
|
| 196 |
flex-wrap: wrap;
|
| 197 |
gap: 10px;
|
| 198 |
}
|
| 199 |
|
| 200 |
-
.timeline-title {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
.emotion-input-container {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
#emotionInput {
|
| 205 |
-
padding: 8px
|
| 206 |
background: var(--bg-tertiary);
|
| 207 |
border: 1px solid var(--border);
|
| 208 |
-
border-radius:
|
| 209 |
color: var(--text-primary);
|
| 210 |
-
font-size: 0.
|
| 211 |
width: 200px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
.timeline-canvas-container {
|
| 215 |
position: relative;
|
| 216 |
background: var(--bg-tertiary);
|
| 217 |
-
border-radius:
|
| 218 |
overflow: hidden;
|
| 219 |
cursor: crosshair;
|
|
|
|
| 220 |
}
|
| 221 |
|
| 222 |
-
#timelineCanvas {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
.instructions {
|
| 225 |
-
margin-top:
|
| 226 |
-
padding:
|
| 227 |
background: var(--bg-tertiary);
|
| 228 |
-
border-radius:
|
| 229 |
-
font-size: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
color: var(--text-secondary);
|
| 231 |
-
line-height: 1.5;
|
| 232 |
}
|
| 233 |
|
| 234 |
-
.instructions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
/* Modal */
|
| 237 |
.modal {
|
| 238 |
display: none;
|
| 239 |
position: fixed;
|
| 240 |
inset: 0;
|
| 241 |
-
background: rgba(0, 0, 0, 0.
|
| 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:
|
| 252 |
padding: 30px;
|
| 253 |
-
max-width:
|
| 254 |
width: 90%;
|
| 255 |
border: 1px solid var(--border);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
-
.
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
-
.
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
| 264 |
|
| 265 |
/* Status */
|
| 266 |
.status-bar {
|
| 267 |
margin-top: 10px;
|
| 268 |
-
padding: 10px;
|
| 269 |
background: var(--bg-tertiary);
|
| 270 |
-
border-radius:
|
| 271 |
-
font-size: 0.
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
| 284 |
<div class="header-controls">
|
| 285 |
-
<button class="btn btn-secondary" onclick="exportJSON()">
|
| 286 |
-
|
| 287 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
| 302 |
<div class="emotion-input-container">
|
| 303 |
-
<input type="text" id="emotionInput" placeholder="
|
| 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 |
-
<
|
| 314 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
let colorIndex = 0;
|
| 366 |
|
| 367 |
-
// Tooltip
|
| 368 |
const tooltip = document.createElement('div');
|
| 369 |
-
tooltip.style.cssText =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
document.body.appendChild(tooltip);
|
| 371 |
|
| 372 |
-
function showTooltip(
|
| 373 |
-
tooltip.
|
| 374 |
tooltip.style.display = 'block';
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 468 |
const maxInt = Math.max(...emotionsAtWord.map(e => e.intensity));
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
}
|
| 472 |
}
|
| 473 |
-
|
| 474 |
const escapedText = word.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 475 |
-
|
|
|
|
| 476 |
lastIndex = word.end;
|
| 477 |
});
|
| 478 |
-
|
| 479 |
-
const trailing = state.text.substring(lastIndex)
|
|
|
|
|
|
|
|
|
|
| 480 |
html += trailing;
|
| 481 |
-
|
| 482 |
-
editorOverlay.innerHTML = html
|
| 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)
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
});
|
| 500 |
span.addEventListener('mouseleave', hideTooltip);
|
| 501 |
});
|
| 502 |
}
|
| 503 |
|
| 504 |
-
// Canvas Setup
|
| 505 |
function resizeCanvas() {
|
| 506 |
const container = canvas.parentElement;
|
| 507 |
canvas.width = container.clientWidth;
|
| 508 |
-
canvas.height =
|
| 509 |
}
|
| 510 |
|
| 511 |
function wordIndexToX(wordIndex) {
|
| 512 |
const totalWords = state.words.length || 1;
|
| 513 |
-
const padding =
|
| 514 |
return padding + (wordIndex / totalWords) * availableWidth;
|
| 515 |
}
|
| 516 |
|
| 517 |
function xToWordIndex(x) {
|
| 518 |
-
const padding =
|
| 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 =
|
| 526 |
return padding + availableHeight - (intensity * availableHeight);
|
| 527 |
}
|
| 528 |
|
| 529 |
function yToIntensity(y) {
|
| 530 |
-
const padding =
|
| 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 |
-
|
|
|
|
| 552 |
if (activeTrack) {
|
| 553 |
const wordIndex = xToWordIndex(coords.x);
|
| 554 |
-
const newPoint = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
| 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 = '
|
| 570 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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();
|
|
|
|
|
|
|
| 595 |
}
|
| 596 |
|
| 597 |
-
function handleCanvasMouseUp() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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();
|
| 605 |
-
|
|
|
|
|
|
|
| 606 |
}
|
| 607 |
}
|
| 608 |
|
|
@@ -633,72 +1168,240 @@
|
|
| 633 |
}
|
| 634 |
|
| 635 |
function drawGrid() {
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
}
|
| 646 |
}
|
| 647 |
|
| 648 |
function drawTrack(track) {
|
| 649 |
if (track.points.length === 0) return;
|
| 650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
ctx.beginPath();
|
| 652 |
track.points.forEach((point, index) => {
|
| 653 |
-
const x = wordIndexToX(point.wordIndex)
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
ctx.stroke();
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 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)
|
| 668 |
-
|
| 669 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
if (point.fadeIn > 0) {
|
| 671 |
const fadeInX = wordIndexToX(point.wordIndex - point.fadeIn);
|
| 672 |
-
ctx.beginPath();
|
| 673 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
}
|
| 675 |
if (point.fadeOut > 0) {
|
| 676 |
const fadeOutX = wordIndexToX(point.wordIndex + point.fadeOut);
|
| 677 |
-
ctx.beginPath();
|
| 678 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
}
|
| 680 |
}
|
| 681 |
|
| 682 |
function drawCursorIndicator() {
|
| 683 |
if (state.words.length === 0) return;
|
| 684 |
const x = wordIndexToX(state.cursorWordIndex);
|
| 685 |
-
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
}
|
| 688 |
|
| 689 |
-
function
|
| 690 |
const activeEmotions = getActiveEmotionsAtWord(state.cursorWordIndex);
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
activeEmotions.forEach(e => {
|
| 695 |
const rgb = hexToRgb(e.color);
|
| 696 |
-
r += rgb.r * e.intensity;
|
|
|
|
|
|
|
|
|
|
| 697 |
});
|
| 698 |
-
|
| 699 |
-
|
|
|
|
|
|
|
|
|
|
| 700 |
const avgInt = total / activeEmotions.length;
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 710 |
if (wordIndex >= start && wordIndex <= end) {
|
| 711 |
let intensity = point.intensity;
|
| 712 |
-
if (wordIndex < point.wordIndex && point.fadeIn > 0)
|
| 713 |
-
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
}
|
| 716 |
});
|
| 717 |
});
|
| 718 |
return activeEmotions;
|
| 719 |
}
|
| 720 |
|
| 721 |
-
//
|
| 722 |
function addEmotionTrack() {
|
| 723 |
const emotionName = emotionInput.value.trim();
|
| 724 |
if (!emotionName) { showStatus('Please enter an emotion name', 'error'); return; }
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
emotionInput.value = '';
|
| 727 |
-
renderTracksList();
|
| 728 |
-
|
|
|
|
|
|
|
| 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 = `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
tracksList.appendChild(item);
|
| 738 |
});
|
| 739 |
}
|
| 740 |
|
| 741 |
function deleteTrack(trackId) {
|
| 742 |
state.tracks = state.tracks.filter(t => t.id !== trackId);
|
| 743 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
}
|
| 745 |
|
| 746 |
-
// Data Export
|
| 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);
|
|
|
|
|
|
|
| 767 |
showStatus('Dataset exported successfully', 'success');
|
| 768 |
}
|
| 769 |
|
| 770 |
function clearAll() {
|
| 771 |
-
if (confirm('
|
| 772 |
localStorage.removeItem('self_distillation_data');
|
| 773 |
-
state.text = '';
|
| 774 |
-
|
| 775 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
}
|
| 777 |
}
|
| 778 |
|
| 779 |
-
// HuggingFace
|
| 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
|
| 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
|
| 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 |
-
//
|
| 832 |
function saveToLocalStorage() {
|
| 833 |
-
localStorage.setItem('self_distillation_data', JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 || '';
|
| 842 |
-
state.
|
| 843 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ? {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
}
|
| 851 |
|
| 852 |
function showStatus(message, type) {
|
| 853 |
-
statusBar.textContent = message;
|
|
|
|
| 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, ' ')
|
| 926 |
+
.replace(/\n/g, '<br>')
|
| 927 |
+
.replace(/\t/g, ' ');
|
| 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 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, ' ')
|
| 971 |
+
.replace(/\n/g, '<br>')
|
| 972 |
+
.replace(/\t/g, ' ');
|
| 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}">×</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 |
|