| <!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, ' ').replace(/\n/g, '<br>').replace(/\t/g, ' '); |
| 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,'&').replace(/</g,'<').replace(/>/g,'>'); |
| html += `<span class="${cls}" style="${style}">${escaped}</span>`; |
| lastIndex = word.end; |
| }); |
| const trailing = state.text.substring(lastIndex) |
| .replace(/ /g,' ').replace(/\n/g,'<br>').replace(/\t/g,' '); |
| 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; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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}">×</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'); |
| |
| |
| const bytes = new TextEncoder().encode(content); |
| const base64 = btoa(Array.from(bytes, b => String.fromCharCode(b)).join('')); |
| |
| |
| 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> |
|
|