| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <title>Video Fit Flow</title> |
| |
| <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.5/js/jsplumb.min.js"></script> |
| <style> |
| html, body { margin:0; padding:1rem; font-family:sans-serif; height:100%; } |
| #wrapper { display:flex; flex-direction:column; height:100vh; overflow:hidden; } |
| #chartContainer { flex:1; position:relative; background:#f5f5f5; overflow:auto; } |
| .node { |
| position:absolute; width:240px; padding:1rem; |
| border:2px solid #888; border-radius:6px; background:#fff; |
| text-align:center; transition:background .3s, border-color .3s; |
| } |
| .completed { border-color:#28a745; background:#e6ffed; } |
| .title-label { background:#e0f7fa; padding:.2rem .5rem; border-radius:4px; margin-top:.5rem; } |
| .score-box { margin-top:.5rem; font-size:1.2rem; } |
| #descContainer { |
| flex:none; border-top:2px solid #888; background:#fff; |
| padding:1rem; max-height:200px; overflow-y:auto; |
| } |
| .controls input, |
| .controls select, |
| .controls button { |
| width:90%; margin:6px auto; display:block; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="wrapper"> |
| |
| <div id="chartContainer"></div> |
|
|
| |
| <div id="descContainer"> |
| <strong>Description:</strong> |
| <div id="videoDesc"></div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const nodes = [ |
| { id:'url', label:'Enter URL & Goal', controls:true }, |
| { id:'meta', label:'YouTube API', showTitle:true } |
| ]; |
| const models = [ |
| "all-MiniLM-L6-v2","multi-qa-MiniLM-L6-cos-v1", |
| "paraphrase-MiniLM-L3-v2","all-mpnet-base-v2", |
| "distilbert-base-nli-mean-tokens" |
| ]; |
| models.forEach(m=>{ |
| nodes.push({ id:`mod-${m}`, label:m }); |
| nodes.push({ id:`scr-${m}`, label:'Score', hasScore:true }); |
| }); |
| const edges = [ |
| { v:'url', w:'meta' }, |
| ...models.flatMap(m=>[ |
| { v:'meta', w:`mod-${m}` }, |
| { v:`mod-${m}`, w:`scr-${m}` } |
| ]) |
| ]; |
| |
| |
| const g = new dagre.graphlib.Graph(); |
| g.setGraph({ rankdir:'TB', marginx:20, marginy:20 }); |
| g.setDefaultEdgeLabel(()=>({})); |
| nodes.forEach(n=>{ |
| const h = n.controls ? 160 : (n.showTitle ? 100 : 50); |
| g.setNode(n.id, { label:n.label, width:240, height:h }); |
| }); |
| edges.forEach(e=>g.setEdge(e.v,e.w)); |
| dagre.layout(g); |
| |
| |
| const chart = document.getElementById('chartContainer'); |
| nodes.forEach(n=>{ |
| const { x,y,width,height } = g.node(n.id); |
| const d = document.createElement('div'); |
| d.id = `node-${n.id}`; d.className='node'; |
| d.style.left = `${x-width/2}px`; |
| d.style.top = `${y-height/2}px`; |
| |
| if (n.controls) { |
| d.innerHTML = ` |
| <strong>${n.label}</strong> |
| <div class="controls"> |
| <input id="urlInput" placeholder="YouTube URL" /> |
| <input id="goalInput" placeholder="Your Goal" /> |
| <button id="btnFetchMeta">Fetch Meta</button> |
| <button id="btnScore" disabled>Generate Score</button> |
| </div>`; |
| } |
| else if (n.showTitle) { |
| d.innerHTML = ` |
| <strong>YouTube API</strong><br/> |
| <div class="title-label">Title:</div> |
| <div id="videoTitle"></div>`; |
| } |
| else { |
| d.innerHTML = `<strong>${n.label}</strong>` + |
| (n.hasScore ? `<div id="score-${n.id}" class="score-box">–</div>` : ''); |
| } |
| chart.appendChild(d); |
| }); |
| |
| |
| jsPlumb.ready(()=>{ |
| const inst = jsPlumb.getInstance({ |
| Connector:['Flowchart',{ cornerRadius:5 }], |
| Anchors:['Bottom','Top'], |
| Endpoint:'Dot', |
| PaintStyle:{ stroke:'#888', strokeWidth:2 }, |
| EndpointStyle:{ fill:'#888', radius:3 } |
| }); |
| |
| edges.forEach(e=>{ |
| inst.connect({ source:`node-${e.v}`, target:`node-${e.w}` }); |
| }); |
| |
| inst.bind('beforeDrop', info=> |
| inst.select({ source: info.sourceId, target: info.targetId }).length === 0 |
| ); |
| |
| new ResizeObserver(()=>inst.repaintEverything()) |
| .observe(document.getElementById('chartContainer')); |
| }); |
| |
| |
| let cached = null; |
| document.getElementById('chartContainer').addEventListener('click', async e=>{ |
| if (e.target.id === 'btnFetchMeta') { |
| |
| const url = document.getElementById('urlInput').value; |
| const res = await fetch('/api/meta', { |
| method:'POST', headers:{'Content-Type':'application/json'}, |
| body: JSON.stringify({url}) |
| }); |
| const data = await res.json(); |
| cached = data; |
| |
| document.getElementById('videoTitle').textContent = data.title; |
| document.getElementById('videoDesc').textContent = data.description; |
| document.getElementById('node-meta').classList.add('completed'); |
| |
| document.getElementById('btnScore').disabled = false; |
| document.getElementById('node-url').classList.add('completed'); |
| } |
| |
| if (e.target.id === 'btnScore' && cached) { |
| const goal = document.getElementById('goalInput').value; |
| const res = await fetch('/api/score', { |
| method:'POST', headers:{'Content-Type':'application/json'}, |
| body: JSON.stringify({ |
| title: cached.title, |
| description: cached.description, |
| goal |
| }) |
| }); |
| const out = await res.json(); |
| models.forEach(m=>{ |
| const sc = out.scores[m]; |
| const sd = document.getElementById(`score-scr-${m}`); |
| sd.textContent = sc; |
| const nd = document.getElementById(`node-scr-${m}`); |
| nd.style.background = sc < 70 ? '#ff6347' : '#90ee90'; |
| nd.classList.add('completed'); |
| }); |
| } |
| }); |
| </script> |
|
|
| </body> |
| </html> |
|
|