Data / index.html
wop's picture
Words now interpolate along bezier curve between points instead of only at exact positions
26553ba
Raw
History Blame Contribute Delete
49.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Self-Distillation</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #08080d;
--bg-secondary: #0e0e16;
--bg-tertiary: #16161f;
--bg-card: #111119;
--text-primary: #e0e0e8;
--text-secondary: #6b6b80;
--text-muted: #44445a;
--accent: #6366f1;
--border: #1e1e2e;
--border-subtle: #181828;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 16px;
gap: 12px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 18px;
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border);
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.header-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.header h1 {
font-size: 1.2rem;
font-weight: 600;
background: linear-gradient(135deg, #c7c8ff, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.header-controls { display: flex; gap: 8px; }
.btn {
padding: 7px 14px;
border: none;
border-radius: 7px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s ease;
font-family: inherit;
display: flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1, #7c3aed);
color: white;
}
.btn-primary:hover { background: linear-gradient(135deg, #5558e3, #6d28d9); transform: translateY(-1px); }
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
.btn-secondary:hover { background: #1e1e2e; }
.btn-danger { background: rgba(127,29,29,0.5); color: #fca5a5; border: 1px solid rgba(153,27,27,0.5); }
.btn-danger:hover { background: rgba(153,27,27,0.6); }
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 200px;
gap: 8px;
}
.editor-wrapper {
flex: 1;
position: relative;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--bg-card);
overflow: hidden;
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.editor-wrapper:focus-within {
border-color: rgba(99, 102, 241, 0.25);
}
.editor-stats {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.72rem;
color: var(--text-muted);
background: linear-gradient(to top, var(--bg-card) 70%, transparent);
z-index: 3;
pointer-events: none;
user-select: none;
}
.active-emotions { display: flex; gap: 6px; align-items: center; }
.active-emotion-pill {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 7px;
border-radius: 8px;
font-size: 0.68rem;
font-weight: 500;
background: rgba(255,255,255,0.04);
}
.active-emotion-dot {
width: 5px;
height: 5px;
border-radius: 50%;
}
#editorOverlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
padding: 22px 22px 36px 22px;
font-size: 1.05rem;
line-height: 1.75;
font-family: inherit;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
overflow-y: auto;
pointer-events: none;
user-select: none;
z-index: 1;
letter-spacing: 0.01em;
}
#editorOverlay .word {
padding: 1px 2px;
margin: 0 -2px;
border-radius: 3px;
transition: background-color 0.2s ease;
}
#editorOverlay .word.has-emotion {
border-bottom: 1.5px solid currentColor;
border-bottom-color: inherit;
}
.placeholder-text {
color: var(--text-muted) !important;
font-style: italic;
font-weight: 300;
}
#editor {
width: 100%;
height: 100%;
background: transparent;
color: transparent;
caret-color: #9194f8;
border: none;
padding: 22px 22px 36px 22px;
font-size: 1.05rem;
line-height: 1.75;
font-family: inherit;
resize: none;
outline: none;
position: absolute;
top: 0; left: 0;
z-index: 2;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
letter-spacing: 0.01em;
}
#editor::selection {
background: rgba(99, 102, 241, 0.3);
}
#editor::-webkit-scrollbar,
#editorOverlay::-webkit-scrollbar { width: 5px; }
#editor::-webkit-scrollbar-track,
#editorOverlay::-webkit-scrollbar-track { background: transparent; }
#editor::-webkit-scrollbar-thumb,
#editorOverlay::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.emotion-sidebar {
position: absolute;
top: 8px; right: 0; bottom: 8px;
width: 3px;
z-index: 3;
pointer-events: none;
border-radius: 0 14px 14px 0;
overflow: hidden;
}
.emotion-sidebar-fill {
position: absolute;
bottom: 0;
width: 100%;
transition: height 0.3s ease, background 0.3s ease;
border-radius: 2px;
}
.tracks-container {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 0 4px;
min-height: 20px;
align-items: center;
user-select: none;
}
.tracks-label {
font-size: 0.72rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.track-item {
display: flex;
align-items: center;
gap: 5px;
background: var(--bg-tertiary);
padding: 4px 10px;
border-radius: 16px;
font-size: 0.78rem;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
user-select: none;
}
.track-item:hover { background: #1e1e2e; }
.track-item.active { border-color: rgba(99,102,241,0.5); }
.track-color {
width: 8px;
height: 8px;
border-radius: 50%;
}
.track-point-count {
font-size: 0.68rem;
color: var(--text-muted);
background: rgba(255,255,255,0.04);
padding: 1px 5px;
border-radius: 6px;
}
.track-delete {
cursor: pointer;
color: var(--text-muted);
font-weight: bold;
transition: color 0.2s;
font-size: 0.85rem;
line-height: 1;
}
.track-delete:hover { color: #ef4444; }
.timeline-section {
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border);
padding: 14px 18px;
flex-shrink: 0;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.timeline-title {
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 7px;
user-select: none;
}
.timeline-title-icon { font-size: 0.85rem; opacity: 0.6; }
.emotion-input-container { display: flex; gap: 8px; align-items: center; }
#emotionInput {
padding: 7px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 7px;
color: var(--text-primary);
font-size: 0.82rem;
width: 190px;
font-family: inherit;
transition: border-color 0.2s;
}
#emotionInput:focus { outline: none; border-color: rgba(99,102,241,0.35); }
.timeline-canvas-container {
position: relative;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
cursor: crosshair;
border: 1px solid var(--border-subtle);
}
#timelineCanvas { display: block; width: 100%; }
.instructions {
margin-top: 10px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 7px;
font-size: 0.72rem;
color: var(--text-muted);
line-height: 1.6;
border: 1px solid var(--border-subtle);
user-select: none;
}
.instructions strong { color: var(--text-secondary); }
.instructions kbd {
display: inline-block;
padding: 1px 4px;
font-size: 0.68rem;
font-family: inherit;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-secondary);
}
.modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active { display: flex; }
.modal-content {
background: var(--bg-secondary);
border-radius: 14px;
padding: 28px;
max-width: 460px;
width: 90%;
border: 1px solid var(--border);
}
.modal-content h2 { margin-bottom: 6px; font-size: 1.15rem; font-weight: 600; }
.modal-subtitle { color: var(--text-muted); font-size: 0.8rem; margin-bottom: 18px; line-height: 1.5; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; margin-bottom: 5px; font-size: 0.8rem; color: var(--text-secondary); font-weight: 500; }
.form-group input {
width: 100%; padding: 9px 12px; background: var(--bg-tertiary);
border: 1px solid var(--border); border-radius: 7px;
color: var(--text-primary); font-size: 0.88rem; font-family: inherit;
}
.form-group input:focus { outline: none; border-color: rgba(99,102,241,0.35); }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 22px; }
.status-bar {
margin-top: 8px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 7px;
font-size: 0.8rem;
color: var(--text-secondary);
display: none;
border: 1px solid var(--border-subtle);
}
.status-bar.active { display: block; }
.status-bar.success { border-left: 3px solid #10b981; color: #6ee7b7; }
.status-bar.error { border-left: 3px solid #ef4444; color: #fca5a5; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-left">
<div class="header-icon"></div>
<h1>Self-Distillation</h1>
</div>
<div class="header-controls">
<button class="btn btn-secondary" onclick="exportJSON()">↓ Export</button>
<button class="btn btn-danger" onclick="clearAll()">✕ Clear</button>
<button class="btn btn-primary" onclick="showHFModal()">⬆ Push to HF</button>
</div>
</div>
<div class="editor-section">
<div class="editor-wrapper" id="editorWrapper">
<div id="editorOverlay"></div>
<textarea id="editor"></textarea>
<div class="editor-stats" id="editorStats">
<span id="wordCount">0 words</span>
<div class="active-emotions" id="activeEmotionsDisplay"></div>
</div>
<div class="emotion-sidebar">
<div class="emotion-sidebar-fill" id="emotionSidebarFill"></div>
</div>
</div>
<div class="tracks-container" id="tracksList">
<span class="tracks-label">Tracks</span>
</div>
</div>
<div class="timeline-section">
<div class="timeline-header">
<div class="timeline-title">
<span class="timeline-title-icon"></span>
Emotional Timeline
</div>
<div class="emotion-input-container">
<input type="text" id="emotionInput" placeholder="e.g., Joy, Anxiety…">
<button class="btn btn-primary" onclick="addEmotionTrack()">+ Add Track</button>
</div>
</div>
<div class="timeline-canvas-container">
<canvas id="timelineCanvas"></canvas>
</div>
<div class="instructions">
<kbd>Click</kbd> timeline to add point ·
<kbd>Drag</kbd> vertically for intensity ·
<kbd>Drag</kbd> handles for fades ·
<kbd>Hover</kbd> points for details ·
<kbd>Double-click</kbd> to remove point
</div>
<div class="status-bar" id="statusBar"></div>
</div>
</div>
<div class="modal" id="hfModal">
<div class="modal-content">
<h2>Push to HuggingFace</h2>
<p class="modal-subtitle">Fetch existing data and append your new entry without overwriting.</p>
<div class="form-group">
<label>HF Token</label>
<input type="password" id="hfToken" placeholder="hf_...">
</div>
<div class="form-group">
<label>Repository ID</label>
<input type="text" id="hfRepoId" placeholder="username/repo-name">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeHFModal()">Cancel</button>
<button class="btn btn-primary" onclick="pushToHF()">Push Dataset</button>
</div>
</div>
</div>
<script>
const state = {
text: '',
words: [],
tracks: [],
selectedPoint: null,
dragging: false,
dragType: null,
dragStart: { x: 0, y: 0 },
cursorWordIndex: 0,
selectedTrackId: null
};
const editor = document.getElementById('editor');
const editorOverlay = document.getElementById('editorOverlay');
const editorWrapper = document.getElementById('editorWrapper');
const canvas = document.getElementById('timelineCanvas');
const ctx = canvas.getContext('2d');
const emotionInput = document.getElementById('emotionInput');
const statusBar = document.getElementById('statusBar');
const tracksList = document.getElementById('tracksList');
const wordCountEl = document.getElementById('wordCount');
const activeEmotionsDisplay = document.getElementById('activeEmotionsDisplay');
const emotionSidebarFill = document.getElementById('emotionSidebarFill');
const colorPalette = [
'#6366f1','#8b5cf6','#ec4899','#f43f5e','#f97316',
'#eab308','#22c55e','#06b6d4','#3b82f6','#a855f7',
'#14b8a6','#f472b6','#fb923c','#a3e635'
];
let colorIndex = 0;
const tooltip = document.createElement('div');
tooltip.style.cssText = `
display:none; position:fixed;
background:rgba(14,14,22,0.95); color:#e8e8f0;
padding:6px 11px; border-radius:7px; font-size:0.73rem;
pointer-events:none; z-index:100; border:1px solid #2a2a3a;
box-shadow:0 6px 20px rgba(0,0,0,0.5);
font-family:'Inter',sans-serif; max-width:240px; line-height:1.4;
`;
document.body.appendChild(tooltip);
function showTooltip(html, x, y) {
tooltip.innerHTML = html;
tooltip.style.display = 'block';
const r = tooltip.getBoundingClientRect();
let left = x + 14, top = y - 10;
if (left + r.width > window.innerWidth - 10) left = x - r.width - 14;
if (top + r.height > window.innerHeight - 10) top = y - r.height - 10;
if (top < 5) top = 5;
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
}
function hideTooltip() { tooltip.style.display = 'none'; }
function init() {
loadFromLocalStorage();
setupEventListeners();
resizeCanvas();
renderTracksList();
updateOverlay();
render();
}
function setupEventListeners() {
editor.addEventListener('input', handleTextChange);
editor.addEventListener('click', handleEditorCursor);
editor.addEventListener('keyup', handleEditorCursor);
editor.addEventListener('scroll', syncScroll);
canvas.addEventListener('mousedown', handleCanvasMouseDown);
canvas.addEventListener('mousemove', handleCanvasMouseMove);
canvas.addEventListener('mouseup', handleCanvasMouseUp);
canvas.addEventListener('mouseleave', () => { handleCanvasMouseUp(); hideTooltip(); });
canvas.addEventListener('dblclick', handleCanvasDoubleClick);
emotionInput.addEventListener('keydown', e => { if (e.key === 'Enter') addEmotionTrack(); });
window.addEventListener('resize', () => { resizeCanvas(); render(); });
}
function syncScroll() {
editorOverlay.scrollTop = editor.scrollTop;
editorOverlay.scrollLeft = editor.scrollLeft;
}
function getWords(text) {
if (!text) return [];
const regex = /\S+/g;
let match, words = [];
while ((match = regex.exec(text)) !== null)
words.push({ start: match.index, end: match.index + match[0].length, text: match[0] });
return words;
}
function handleTextChange() {
state.text = editor.value;
state.words = getWords(state.text);
state.tracks.forEach(t => { t.points = t.points.filter(p => p.wordIndex < state.words.length); });
updateOverlay();
render();
saveToLocalStorage();
}
function handleEditorCursor() {
const ci = editor.selectionStart;
let idx = state.words.length;
for (let i = 0; i < state.words.length; i++) {
if (ci <= state.words[i].end) { idx = i; break; }
}
state.cursorWordIndex = idx;
updateEditorEffects();
render();
}
function updateOverlay() {
let html = '';
let lastIndex = 0;
if (state.words.length === 0) {
editorOverlay.innerHTML = '<span class="placeholder-text">Begin writing your thoughts here…</span>';
wordCountEl.textContent = '0 words';
return;
}
state.words.forEach((word, i) => {
const gap = state.text.substring(lastIndex, word.start)
.replace(/ /g, '&nbsp;').replace(/\n/g, '<br>').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
html += gap;
const emo = getActiveEmotionsAtWord(i);
let style = '';
let cls = 'word';
if (emo.length > 0) {
let r=0,g=0,b=0,total=0;
emo.forEach(e => {
const rgb = hexToRgb(e.color);
r += rgb.r*e.intensity; g += rgb.g*e.intensity; b += rgb.b*e.intensity; total += e.intensity;
});
if (total > 0) {
r=Math.round(r/total); g=Math.round(g/total); b=Math.round(b/total);
const maxI = Math.max(...emo.map(e=>e.intensity));
const bgA = Math.max(0.06, maxI * 0.25);
style += `background:rgba(${r},${g},${b},${bgA});`;
const blend = Math.min(0.45, maxI * 0.4);
const tr = Math.round(224*(1-blend)+r*blend);
const tg = Math.round(224*(1-blend)+g*blend);
const tb = Math.round(232*(1-blend)+b*blend);
style += `color:rgb(${tr},${tg},${tb});`;
style += `border-bottom-color:rgba(${r},${g},${b},${Math.min(0.5, maxI*0.4)});`;
cls += ' has-emotion';
}
}
const escaped = word.text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
html += `<span class="${cls}" style="${style}">${escaped}</span>`;
lastIndex = word.end;
});
const trailing = state.text.substring(lastIndex)
.replace(/ /g,'&nbsp;').replace(/\n/g,'<br>').replace(/\t/g,'&nbsp;&nbsp;&nbsp;&nbsp;');
html += trailing;
editorOverlay.innerHTML = html;
wordCountEl.textContent = `${state.words.length} word${state.words.length!==1?'s':''}`;
}
function resizeCanvas() {
const c = canvas.parentElement;
canvas.width = c.clientWidth;
canvas.height = 175;
}
function wordIndexToX(wi) {
const total = state.words.length || 1, pad = 35, w = canvas.width - pad*2;
return pad + (wi/total)*w;
}
function xToWordIndex(x) {
const pad = 35, w = canvas.width - pad*2, total = state.words.length || 1;
return Math.round(Math.max(0, Math.min(x-pad,w))/w * total);
}
function intensityToY(i) {
const pad = 22, h = canvas.height - pad*2;
return pad + h - i*h;
}
function yToIntensity(y) {
const pad = 22, h = canvas.height - pad*2;
return 1 - (Math.max(pad, Math.min(y, canvas.height-pad)) - pad) / h;
}
function getCanvasCoords(e) {
const r = canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
function handleCanvasMouseDown(e) {
const c = getCanvasCoords(e);
const hit = findItemAt(c.x, c.y);
if (hit) {
state.selectedPoint = hit;
state.dragging = true;
state.dragType = hit.type;
state.dragStart = { x: c.x, y: c.y };
} else {
const track = state.tracks.find(t => t.id === state.selectedTrackId) || state.tracks[state.tracks.length-1];
if (track && state.words.length > 0) {
const pt = {
id: Date.now(),
wordIndex: xToWordIndex(c.x),
intensity: Math.max(0, Math.min(1, yToIntensity(c.y))),
fadeIn: 0, fadeOut: 0
};
track.points.push(pt);
track.points.sort((a,b) => a.wordIndex - b.wordIndex);
state.selectedPoint = { type:'point', point:pt, track };
state.dragging = true;
state.dragType = 'point';
render(); updateOverlay(); renderTracksList(); saveToLocalStorage();
}
}
}
function handleCanvasMouseMove(e) {
const c = getCanvasCoords(e);
if (!state.dragging) {
const hit = findItemAt(c.x, c.y);
if (hit) {
canvas.style.cursor = hit.type === 'point' ? 'grab' : 'ew-resize';
if (hit.type === 'point') {
const pct = (hit.point.intensity*100).toFixed(0);
const wt = state.words[hit.point.wordIndex]?.text || '';
showTooltip(`<div style="font-weight:600;color:${hit.track.color}">${hit.track.label}</div>
<div style="margin-top:2px">Intensity: ${pct}%</div>
${wt ? `<div style="color:#6b6b80">at "${wt}"</div>` : ''}`, e.clientX, e.clientY);
} else {
showTooltip(`Drag to adjust ${hit.type==='fadeIn'?'fade in':'fade out'}`, e.clientX, e.clientY);
}
} else {
canvas.style.cursor = 'crosshair';
hideTooltip();
}
return;
}
if (!state.selectedPoint) return;
canvas.style.cursor = 'grabbing';
if (state.dragType === 'point') {
const pt = state.selectedPoint.point;
pt.wordIndex = Math.max(0, Math.min(state.words.length-1, xToWordIndex(c.x)));
pt.intensity = Math.max(0, Math.min(1, yToIntensity(c.y)));
state.selectedPoint.track.points.sort((a,b) => a.wordIndex - b.wordIndex);
} else {
const dx = c.x - state.dragStart.x;
if (state.dragType === 'fadeIn') {
state.selectedPoint.point.fadeIn = Math.max(0,
Math.min(state.selectedPoint.point.wordIndex, state.selectedPoint.point.fadeIn + dx/50));
} else {
const max = state.words.length - state.selectedPoint.point.wordIndex;
state.selectedPoint.point.fadeOut = Math.max(0,
Math.min(max, state.selectedPoint.point.fadeOut + dx/50));
}
}
state.dragStart = { x: c.x, y: c.y };
render(); updateOverlay(); saveToLocalStorage();
}
function handleCanvasMouseUp() {
state.dragging = false;
state.dragType = null;
canvas.style.cursor = 'crosshair';
}
function handleCanvasDoubleClick(e) {
const c = getCanvasCoords(e);
const hit = findItemAt(c.x, c.y);
if (hit && hit.type === 'point') {
hit.track.points = hit.track.points.filter(p => p.id !== hit.point.id);
render(); updateOverlay(); renderTracksList(); saveToLocalStorage();
showStatus('Point removed', 'success');
}
}
function findItemAt(x, y) {
for (const track of state.tracks) {
for (const pt of track.points) {
const px = wordIndexToX(pt.wordIndex), py = intensityToY(pt.intensity);
if (Math.abs(x-px)<10 && Math.abs(y-py)<10) return { type:'point', point:pt, track };
if (pt.fadeIn > 0) {
const fx = wordIndexToX(pt.wordIndex - pt.fadeIn);
if (Math.abs(x-fx)<8 && Math.abs(y-py)<8) return { type:'fadeIn', point:pt, track };
}
if (pt.fadeOut > 0) {
const fx = wordIndexToX(pt.wordIndex + pt.fadeOut);
if (Math.abs(x-fx)<8 && Math.abs(y-py)<8) return { type:'fadeOut', point:pt, track };
}
}
}
return null;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
state.tracks.forEach(drawTrack);
drawCursor();
}
function drawGrid() {
for (let i = 0; i <= 1; i += 0.25) {
const y = intensityToY(i);
ctx.strokeStyle = i === 0 ? '#1e1e2e' : '#141420';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(35, y); ctx.lineTo(canvas.width-10, y); ctx.stroke();
ctx.fillStyle = '#2e2e42';
ctx.font = '9px Inter, sans-serif';
ctx.textAlign = 'right';
ctx.fillText((i*100).toFixed(0)+'%', 31, y+3);
}
if (state.words.length > 0) {
const step = Math.max(1, Math.floor(state.words.length/20));
for (let i = 0; i <= state.words.length; i += step) {
const x = wordIndexToX(i);
ctx.strokeStyle = '#111120';
ctx.beginPath(); ctx.moveTo(x,22); ctx.lineTo(x,canvas.height-22); ctx.stroke();
}
}
}
function drawTrack(track) {
if (track.points.length === 0) return;
const rgb = hexToRgb(track.color);
const sel = track.id === state.selectedTrackId;
function buildPath() {
ctx.beginPath();
track.points.forEach((pt, i) => {
const x = wordIndexToX(pt.wordIndex), y = intensityToY(pt.intensity);
if (i === 0) { ctx.moveTo(x, y); return; }
const px = wordIndexToX(track.points[i-1].wordIndex);
const py = intensityToY(track.points[i-1].intensity);
const cpx = (px+x)/2;
ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
});
}
buildPath();
ctx.lineTo(wordIndexToX(track.points[track.points.length-1].wordIndex), canvas.height-22);
ctx.lineTo(wordIndexToX(track.points[0].wordIndex), canvas.height-22);
ctx.closePath();
const grad = ctx.createLinearGradient(0, 22, 0, canvas.height-22);
grad.addColorStop(0, `rgba(${rgb.r},${rgb.g},${rgb.b},${sel?0.12:0.06})`);
grad.addColorStop(1, `rgba(${rgb.r},${rgb.g},${rgb.b},0)`);
ctx.fillStyle = grad;
ctx.fill();
buildPath();
ctx.strokeStyle = track.color;
ctx.lineWidth = sel ? 2.5 : 1.8;
ctx.stroke();
track.points.forEach(pt => {
const x = wordIndexToX(pt.wordIndex), y = intensityToY(pt.intensity);
ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2);
ctx.fillStyle = track.color; ctx.fill();
ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI*2);
ctx.fillStyle = '#fff'; ctx.fill();
if (pt.fadeIn > 0) {
const fx = wordIndexToX(pt.wordIndex - pt.fadeIn);
ctx.beginPath(); ctx.arc(fx, y, 3.5, 0, Math.PI*2);
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)`; ctx.fill();
ctx.setLineDash([3,3]); ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.25)`;
ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(fx,y); ctx.lineTo(x,y); ctx.stroke();
ctx.setLineDash([]);
}
if (pt.fadeOut > 0) {
const fx = wordIndexToX(pt.wordIndex + pt.fadeOut);
ctx.beginPath(); ctx.arc(fx, y, 3.5, 0, Math.PI*2);
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)`; ctx.fill();
ctx.setLineDash([3,3]); ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.25)`;
ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(fx,y); ctx.stroke();
ctx.setLineDash([]);
}
});
}
function drawCursor() {
if (state.words.length === 0) return;
const x = wordIndexToX(state.cursorWordIndex);
const grad = ctx.createLinearGradient(x, 0, x, canvas.height);
grad.addColorStop(0, 'rgba(255,255,255,0)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.08)');
grad.addColorStop(0.7, 'rgba(255,255,255,0.08)');
grad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.strokeStyle = grad;
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.beginPath();
ctx.moveTo(x, 6); ctx.lineTo(x+3, 11); ctx.lineTo(x-3, 11);
ctx.closePath(); ctx.fill();
}
function updateEditorEffects() {
const emo = getActiveEmotionsAtWord(state.cursorWordIndex);
if (emo.length === 0) {
editorWrapper.style.boxShadow = 'none';
editorWrapper.style.borderColor = '';
emotionSidebarFill.style.height = '0%';
activeEmotionsDisplay.innerHTML = '';
return;
}
let r=0,g=0,b=0,total=0;
emo.forEach(e => {
const rgb = hexToRgb(e.color);
r+=rgb.r*e.intensity; g+=rgb.g*e.intensity; b+=rgb.b*e.intensity; total+=e.intensity;
});
if (total > 0) {
r=Math.round(r/total); g=Math.round(g/total); b=Math.round(b/total);
const avg = total / emo.length;
editorWrapper.style.boxShadow = `inset 0 0 60px rgba(${r},${g},${b},${avg*0.04})`;
editorWrapper.style.borderColor = `rgba(${r},${g},${b},${Math.max(0.15, avg*0.3)})`;
emotionSidebarFill.style.height = `${avg*100}%`;
emotionSidebarFill.style.background = `linear-gradient(to top, rgba(${r},${g},${b},0.45), rgba(${r},${g},${b},0.1))`;
activeEmotionsDisplay.innerHTML = emo.map(e => `
<div class="active-emotion-pill">
<span class="active-emotion-dot" style="background:${e.color}"></span>
${e.label} ${(e.intensity*100).toFixed(0)}%
</div>`).join('');
}
}
function getActiveEmotionsAtWord(wi) {
const result = [];
state.tracks.forEach(track => {
const sorted = [...track.points].sort((a, b) => a.wordIndex - b.wordIndex);
if (sorted.length === 0) return;
const first = sorted[0], last = sorted[sorted.length - 1];
if (sorted.length === 1) {
const s = first.wordIndex - first.fadeIn, e = first.wordIndex + first.fadeOut;
if (wi >= s && wi <= e) {
let intensity = first.intensity;
if (wi < first.wordIndex && first.fadeIn > 0) intensity *= (wi - s) / first.fadeIn;
if (wi > first.wordIndex && first.fadeOut > 0) intensity *= 1 - (wi - first.wordIndex) / first.fadeOut;
result.push({ label: track.label, color: track.color, intensity: Math.max(0, Math.min(1, intensity)) });
}
return;
}
// Fade-in before first point
if (wi < first.wordIndex && first.fadeIn > 0) {
const s = first.wordIndex - first.fadeIn;
if (wi >= s) {
const intensity = first.intensity * (wi - s) / first.fadeIn;
result.push({ label: track.label, color: track.color, intensity: Math.max(0, Math.min(1, intensity)) });
}
return;
}
// Fade-out after last point
if (wi > last.wordIndex && last.fadeOut > 0) {
const e = last.wordIndex + last.fadeOut;
if (wi <= e) {
const intensity = last.intensity * (1 - (wi - last.wordIndex) / last.fadeOut);
result.push({ label: track.label, color: track.color, intensity: Math.max(0, Math.min(1, intensity)) });
}
return;
}
// Bezier interpolation between consecutive points
if (wi >= first.wordIndex && wi <= last.wordIndex) {
for (let i = 1; i < sorted.length; i++) {
const p0 = sorted[i - 1], p1 = sorted[i];
if (wi >= p0.wordIndex && wi <= p1.wordIndex) {
const x0 = wordIndexToX(p0.wordIndex), y0 = intensityToY(p0.intensity);
const x1 = wordIndexToX(p1.wordIndex), y1 = intensityToY(p1.intensity);
const cx = (x0 + x1) / 2;
const wordX = wordIndexToX(wi);
let t = (x1 === x0) ? 0 : (wordX - x0) / (x1 - x0);
t = Math.max(0, Math.min(1, t));
for (let iter = 0; iter < 5; iter++) {
const u = 1 - t;
const bx = u*u*u*x0 + 3*u*u*t*cx + 3*u*t*t*cx + t*t*t*x1;
const dbx = -3*u*u*x0 + 3*(u*u - 2*u*t)*cx + 3*(2*u*t - t*t)*cx + 3*t*t*x1;
if (dbx === 0) break;
t = Math.max(0, Math.min(1, t - (bx - wordX) / dbx));
}
const u = 1 - t;
const by = u*u*u*y0 + 3*u*u*t*y0 + 3*u*t*t*y1 + t*t*t*y1;
const intensity = 1 - (by - 22) / (canvas.height - 44);
result.push({ label: track.label, color: track.color, intensity: Math.max(0, Math.min(1, intensity)) });
break;
}
}
}
});
return result;
}
function addEmotionTrack() {
const name = emotionInput.value.trim();
if (!name) { showStatus('Enter an emotion name','error'); return; }
const t = { id:Date.now(), label:name, color:colorPalette[colorIndex++%colorPalette.length], points:[] };
state.tracks.push(t);
state.selectedTrackId = t.id;
emotionInput.value = '';
renderTracksList(); render(); saveToLocalStorage();
showStatus(`Track "${name}" added — click timeline to place points`, 'success');
}
function renderTracksList() {
tracksList.innerHTML = '<span class="tracks-label">Tracks</span>';
if (state.tracks.length === 0) {
tracksList.innerHTML += '<span style="font-size:0.78rem;color:var(--text-muted);font-style:italic">No tracks yet</span>';
return;
}
state.tracks.forEach(track => {
const item = document.createElement('div');
item.className = 'track-item' + (track.id===state.selectedTrackId?' active':'');
item.innerHTML = `
<div class="track-color" style="background:${track.color}"></div>
<span>${track.label}</span>
<span class="track-point-count">${track.points.length}pt</span>
<span class="track-delete" data-id="${track.id}">&times;</span>`;
item.addEventListener('click', e => {
if (e.target.classList.contains('track-delete')) return;
state.selectedTrackId = track.id;
renderTracksList(); render();
});
item.querySelector('.track-delete').addEventListener('click', e => {
e.stopPropagation(); deleteTrack(track.id);
});
tracksList.appendChild(item);
});
}
function deleteTrack(id) {
state.tracks = state.tracks.filter(t => t.id !== id);
if (state.selectedTrackId === id)
state.selectedTrackId = state.tracks.length ? state.tracks[state.tracks.length-1].id : null;
renderTracksList(); render(); updateOverlay(); saveToLocalStorage();
}
function generateDataset() {
const ef = [];
state.tracks.forEach(track => {
const curves = track.points.map(pt => ({
start_word: Math.max(0, pt.wordIndex - pt.fadeIn),
end_word: Math.min(state.words.length, pt.wordIndex + pt.fadeOut),
start_intensity: pt.fadeIn > 0 ? 0 : pt.intensity,
peak_intensity: pt.intensity,
end_intensity: pt.fadeOut > 0 ? 0 : pt.intensity
}));
if (curves.length > 0) ef.push({ label: track.label, curves });
});
return { text: state.text, emotional_flow: ef };
}
function exportJSON() {
const s = JSON.stringify(generateDataset(), null, 2);
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([s],{type:'application/json'}));
a.download = 'self-distillation-dataset.json'; a.click();
showStatus('Dataset exported','success');
}
function clearAll() {
if (!confirm('Clear all text and emotion tracks?')) return;
localStorage.removeItem('self_distillation_data');
state.text=''; state.tracks=[]; state.words=[]; state.cursorWordIndex=0; state.selectedTrackId=null;
editor.value='';
editorWrapper.style.boxShadow='none'; editorWrapper.style.borderColor='';
emotionSidebarFill.style.height='0%'; activeEmotionsDisplay.innerHTML='';
updateOverlay(); renderTracksList(); render();
showStatus('All data cleared','success');
}
function showHFModal() {
const t = localStorage.getItem('hf_token'), r = localStorage.getItem('hf_repo_id');
if (t) document.getElementById('hfToken').value = t;
if (r) document.getElementById('hfRepoId').value = r;
document.getElementById('hfModal').classList.add('active');
}
function closeHFModal() { document.getElementById('hfModal').classList.remove('active'); }
async function pushToHF() {
const token = document.getElementById('hfToken').value.trim();
const repoId = document.getElementById('hfRepoId').value.trim();
if (!token || !repoId) { showStatus('Provide both token and repo ID', 'error'); return; }
localStorage.setItem('hf_token', token);
localStorage.setItem('hf_repo_id', repoId);
try {
showStatus('Fetching existing dataset…', 'success');
let existing = [];
try {
const res = await fetch(`https://huggingface.co/datasets/${repoId}/resolve/main/data.json?_${Date.now()}`, {
headers: { "Authorization": `Bearer ${token}`, "Cache-Control": "no-cache" }
});
if (res.ok) {
const text = await res.text();
if (text.trim()) {
existing = JSON.parse(text);
if (!Array.isArray(existing)) existing = [existing];
}
}
} catch (e) { console.log('No existing file, starting fresh.'); }
existing.push(generateDataset());
const content = JSON.stringify(existing, null, 2);
showStatus('Uploading…', 'success');
// Base64 encode the UTF-8 content
const bytes = new TextEncoder().encode(content);
const base64 = btoa(Array.from(bytes, b => String.fromCharCode(b)).join(''));
// HuggingFace commit API expects NDJSON (newline-delimited JSON), not regular JSON
const ndjson = [
JSON.stringify({
key: "header",
value: {
summary: "Append self-distillation entry",
description: "Update data.json via Self-Distillation app"
}
}),
JSON.stringify({
key: "file",
value: {
path: "data.json",
content: base64,
encoding: "base64"
}
})
].join('\n');
const r = await fetch(`https://huggingface.co/api/datasets/${repoId}/commit/main`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/x-ndjson"
},
body: ndjson
});
if (!r.ok) {
const errText = await r.text();
console.error('HF response:', r.status, errText);
throw new Error(`HF API ${r.status}: ${errText}`);
}
const result = await r.json();
console.log('HF result:', result);
showStatus('Pushed successfully!', 'success');
closeHFModal();
} catch (e) {
showStatus(`Error: ${e.message}`, 'error');
}
}
function saveToLocalStorage() {
localStorage.setItem('self_distillation_data', JSON.stringify({
text:state.text, tracks:state.tracks, colorIndex, selectedTrackId:state.selectedTrackId
}));
}
function loadFromLocalStorage() {
const s = localStorage.getItem('self_distillation_data');
if (s) try {
const d = JSON.parse(s);
state.text=d.text||''; state.tracks=d.tracks||[]; colorIndex=d.colorIndex||0;
state.selectedTrackId=d.selectedTrackId||null;
state.words=getWords(state.text); editor.value=state.text;
} catch(e) {}
}
function hexToRgb(hex) {
const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return r ? {r:parseInt(r[1],16),g:parseInt(r[2],16),b:parseInt(r[3],16)} : {r:0,g:0,b:0};
}
function showStatus(msg, type) {
statusBar.textContent = msg;
statusBar.className = `status-bar active ${type}`;
setTimeout(() => statusBar.classList.remove('active'), 3000);
}
init();
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a0b07b9bdd9ed0fb',t:'MTc4MTM0NzI4Ng=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>