|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"/> |
|
<title>Video Fit Flow</title> |
|
|
|
<script src="https://unpkg.com/[email protected]/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> |
|
|