Download index.html from wop/Data: direct link, hf CLI and curl.
- Browser
- Download file 49.8 kB
-
https://huggingface.co/spaces/wop/Data/resolve/main/index.html
- Command line
-
hf download hf://spaces/wop/Data/index.html
-
curl -L -o index.html https://huggingface.co/spaces/wop/Data/resolve/main/index.html
49.8 kB
| <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) ; | |
| 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; | |
| } | |
| // 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}">×</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> | |