|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Pipeline Graph</title> |
|
<script src="https://d3js.org/d3.v7.min.js"></script> |
|
<style> |
|
* { |
|
font-family: Arial, sans-serif; |
|
} |
|
|
|
.container { |
|
width: 95%; |
|
height: 40vh; |
|
margin: 20px auto; |
|
position: relative; |
|
} |
|
.title { |
|
font: 24px/1.5 'Arial', sans-serif; |
|
color: #6b7b8c; |
|
text-align: center; |
|
padding: 12px 0; |
|
} |
|
.node rect { |
|
rx: 8px; |
|
ry: 8px; |
|
stroke-width: 1.5; |
|
} |
|
.node text { |
|
font: 12px sans-serif; |
|
pointer-events: none; |
|
text-anchor: middle; |
|
dominant-baseline: central; |
|
} |
|
.link { |
|
fill: none; |
|
stroke-width: 2; |
|
stroke-opacity: 0.6; |
|
} |
|
.tooltip { |
|
position: absolute; |
|
padding: 8px; |
|
background: #fff9ec; |
|
border: 1px solid #ffd8a8; |
|
border-radius: 4px; |
|
box-shadow: 3px 3px 10px rgba(0,0,0,0.1); |
|
font: 12px/1.5 sans-serif; |
|
color: #66512c; |
|
} |
|
.lower-container { |
|
width: 95%; |
|
height: 45vh; |
|
margin: 20px auto; |
|
display: flex; |
|
gap: 20px; |
|
} |
|
|
|
.doc-panel, .info-panel { |
|
height: 50vh; |
|
flex: 1; |
|
background: #f8f9fa; |
|
border: 2px solid #B5C4B8; |
|
border-radius: 12px; |
|
padding: 15px; |
|
} |
|
|
|
#doc-list { |
|
max-height: 95%; |
|
overflow-y: auto; |
|
} |
|
.qa-box { |
|
border-top: 2px solid #ecf0f1; |
|
padding-top: 15px; |
|
padding-bottom: 15px; |
|
margin-bottom: 15px; |
|
max-height: 30%; |
|
overflow-y: auto; |
|
} |
|
|
|
.qa-question { |
|
color: #2c3e50; |
|
font-weight: 600; |
|
margin-bottom: 12px; |
|
max-height: 10%; |
|
overflow-y: auto; |
|
} |
|
|
|
.qa-answer { |
|
color: #34495e; |
|
line-height: 1.6; |
|
} |
|
|
|
|
|
|
|
|
|
#output-box { |
|
border-top: 2px solid #ecf0f1; |
|
padding-top: 15px; |
|
max-height: 50%; |
|
overflow-y: auto; |
|
} |
|
|
|
.doc-item { |
|
border: 1px solid #B5C4B8; |
|
border-radius: 8px; |
|
padding: 12px; |
|
margin-bottom: 10px; |
|
background: #fff; |
|
box-shadow: 0 2px 6px rgba(175, 189, 188, 0.1); |
|
} |
|
.color-display { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
margin-left: 20px; |
|
flex: 1; |
|
margin-right: 20px; |
|
} |
|
|
|
|
|
#control_container{ |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
width: 100%; |
|
height: 15%; |
|
|
|
} |
|
|
|
|
|
|
|
.color-box { |
|
width: 24px; |
|
height: 24px; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
} |
|
|
|
.value-display { |
|
font-size: 0.9em; |
|
min-width: 120px; |
|
color: #666; |
|
} |
|
|
|
.sentence-box { |
|
border: 1px solid #e0e7e9; |
|
border-radius: 6px; |
|
padding: 10px; |
|
margin: 8px 0; |
|
background: #f8fafb; |
|
line-height: 1.5; |
|
} |
|
.output-line:hover { |
|
background: #f0f0f0; |
|
} |
|
|
|
.output-line { |
|
border-left: 3px solid #B5C4B8; |
|
margin: 6px 0; |
|
cursor: pointer; |
|
padding: 8px 12px; |
|
background: #fdfdfd; |
|
border-radius: 4px; |
|
} |
|
.highlight-span { |
|
cursor: pointer; |
|
} |
|
.highlight-span:hover { |
|
filter: brightness(1.1); |
|
} |
|
.filter-slider { |
|
margin-top: 15px; |
|
width: 250px; |
|
flex: 1; |
|
display: none; |
|
} |
|
|
|
.slider-header { |
|
display: flex; |
|
justify-content: space-between; |
|
margin-bottom: 8px; |
|
font-size: 0.9em; |
|
color: #666; |
|
} |
|
|
|
#threshold { |
|
width: 100%; |
|
height: 4px; |
|
background: #ddd; |
|
border-radius: 2px; |
|
outline: none; |
|
} |
|
|
|
#threshold::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 16px; |
|
height: 16px; |
|
background: #ff4444; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="graph-container" class="container"></div> |
|
<div id="tooltip" class="tooltip" style="opacity:0"></div> |
|
|
|
|
|
|
|
|
|
<div class="lower-container"> |
|
|
|
<div class="doc-panel"> |
|
<div class="panel-title"> Documents</div> |
|
<div id="doc-list" class="doc-list"></div> |
|
</div> |
|
|
|
|
|
<div class="info-panel"> |
|
|
|
<div class="qa-box"> |
|
<div id="current-question" class="qa-question"></div> |
|
<div id="current-answer" class="qa-answer"></div> |
|
</div> |
|
|
|
|
|
<div class="control-box"> |
|
<label>Show Result: |
|
<select id="result-select" onchange="updateResult(this.value)"> |
|
<option value="0">Result 1</option><option value="1">Result 2</option> |
|
</select> |
|
</label> |
|
<label style="margin-left:20px">Granularity: |
|
<select id="granularity"> |
|
<option>Document-level</option> |
|
<option>Span-level</option> |
|
<option>Word-level</option> |
|
</select> |
|
</label> |
|
<div class="container" id="control_container"> |
|
<div class="color-display"> |
|
<div class="color-box"></div> |
|
<div class="value-display">Click document to show value</div> |
|
</div> |
|
|
|
<div class="filter-slider" style="display:none"> |
|
<div class="slider-header"> |
|
<span>Filter Threshold: </span> |
|
<span id="threshold-value">0.00</span> |
|
</div> |
|
<input type="range" id="threshold" min="0" max="1" step="0.01" value="0"> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="output-box" class="output-box"></div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const nodes = [{"id": "input", "type": "input", "params": {}}, {"id": "output", "type": "output", "params": {}}, {"id": "gpt-3.5-turbo-[1]", "type": "Generator", "params": {"type": "Generator", "Model": "gpt-3.5-turbo", "Max Turn": 6}}, {"id": "retriever-[4]", "type": "retriever", "params": {"type": "retriever", "Max Turn": 6}}, {"id": "gpt-3.5-turbo-[3]", "type": "Generator", "params": {"type": "Generator", "Model": "gpt-3.5-turbo", "Mode": "parallel", "Max Turn": 10}}, {"id": "evaluator-[2]", "type": "evaluator", "params": {"type": "evaluator", "Mode": "iterative", "Max Turn": 5}}]; |
|
const edges = [{"source": "input", "target": "gpt-3.5-turbo-[1]", "weight": 0}, {"source": "evaluator-[2]", "target": "gpt-3.5-turbo-[1]", "weight": 0}, {"source": "evaluator-[2]", "target": "output", "weight": 0}, {"source": "gpt-3.5-turbo-[3]", "target": "evaluator-[2]", "weight": 0}, {"source": "retriever-[4]", "target": "gpt-3.5-turbo-[3]", "weight": 0}, {"source": "gpt-3.5-turbo-[1]", "target": "retriever-[4]", "weight": 0}]; |
|
const colors = ['#B5C4B8', '#D3C0B6', '#A9B7C4', '#C4A9B7', '#B7C4A9']; |
|
|
|
const container = d3.select("#graph-container"); |
|
const width = container.node().offsetWidth; |
|
const height = container.node().offsetHeight; |
|
let currentGranularity = "Document-level"; |
|
|
|
let currentSentenceIndex = null; |
|
let currentDocValues = []; |
|
|
|
let currentThreshold = 0; |
|
|
|
|
|
d3.select("#threshold") |
|
.on("input", function() { |
|
currentThreshold = parseFloat(this.value); |
|
d3.select("#threshold-value").text(currentThreshold.toFixed(2)); |
|
applyThresholdFilter(); |
|
}); |
|
|
|
|
|
function applyThresholdFilter() { |
|
d3.selectAll(".highlight-span").each(function() { |
|
const rawValue = parseFloat(this.dataset.value); |
|
const originalColor = this.dataset.originalColor; |
|
const shouldShow = rawValue >= currentThreshold; |
|
|
|
d3.select(this) |
|
.style("background", shouldShow ? originalColor : "white") |
|
.classed("filtered-out", !shouldShow); |
|
}); |
|
} |
|
function handleSpanClick(event, rawValue) { |
|
event.stopPropagation(); |
|
|
|
d3.select(".color-box") |
|
.style("background", event.target.style.background) |
|
.style("border-color", "#999"); |
|
|
|
d3.select(".value-display") |
|
.text(`Value: ${rawValue.toFixed(4)}`) |
|
.style("color", "#333"); |
|
} |
|
const svg = container.append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
|
|
svg.append("defs").append("marker") |
|
.attr("id", "arrow") |
|
.attr("viewBox", "0 -5 10 10") |
|
.attr("refX", 32) |
|
.attr("markerWidth", 8) |
|
.attr("markerHeight", 8) |
|
.attr("orient", "auto") |
|
.append("path") |
|
.attr("d", "M0,-4L8,0L0,4") |
|
.attr("fill", "#7E8A97"); |
|
|
|
svg.append("rect") |
|
.attr("width", width-2) |
|
.attr("height", height-2) |
|
.attr("x", 1) |
|
.attr("y", 1) |
|
.attr("rx", 12) |
|
.attr("ry", 12) |
|
.attr("fill", "none") |
|
.attr("stroke", "#B5C4B8") |
|
.attr("stroke-width", 2) |
|
.lower(); |
|
|
|
|
|
|
|
const simulation = d3.forceSimulation(nodes) |
|
.force("link", d3.forceLink(edges).id(d => d.id).distance(120)) |
|
.force("charge", d3.forceManyBody().strength(-400)) |
|
.force("collide", d3.forceCollide().radius(42)) |
|
.force("x", d3.forceX() |
|
.x(d => { |
|
if (d.id === 'input') return width * 0.1; |
|
if (d.id === 'output') return width * 0.9; |
|
return width / 2; |
|
}) |
|
.strength(0.1)) |
|
.force("y", d3.forceY(height/2).strength(0.05)) |
|
.force("center", null); |
|
|
|
const link = svg.selectAll(".link") |
|
.data(edges) |
|
.enter().append("line") |
|
.attr("class", "link") |
|
.attr("stroke", d => d.weight === 1 ? "green" : d.weight === -1 ? "red" : "black") |
|
.attr("marker-end", "url(#arrow)") |
|
.style("stroke-dasharray", d => d.weight === 0 ? "4 4" : null); |
|
|
|
const node = svg.selectAll(".node") |
|
.data(nodes) |
|
.enter().append("g") |
|
.attr("class", "node") |
|
.call(d3.drag() |
|
.on("start", dragstart) |
|
.on("drag", dragging) |
|
.on("end", dragend)) |
|
.on("mouseover", showTooltip) |
|
.on("mouseout", hideTooltip); |
|
|
|
node.append("rect") |
|
.attr("width", 80) |
|
.attr("height", 36) |
|
.attr("x", -40) |
|
.attr("y", -18) |
|
.attr("fill", d => colors[d.type.length % colors.length]) |
|
.attr("stroke", "#7E8A97"); |
|
|
|
node.append("text") |
|
.text(d => d.type) |
|
.attr("dy", 0); |
|
|
|
simulation.on("tick", () => { |
|
link.attr("x1", d => d.source.x) |
|
.attr("y1", d => d.source.y) |
|
.attr("x2", d => d.target.x) |
|
.attr("y2", d => d.target.y); |
|
node.attr("transform", d => `translate(${d.x},${d.y})`); |
|
}); |
|
|
|
const tooltip = d3.select("#tooltip"); |
|
function showTooltip(event, d) { |
|
tooltip.transition().style("opacity", 0.9); |
|
const params = Object.entries(d.params) |
|
.filter(([k]) => k !== 'type') |
|
.map(([k,v]) => `${k}: ${v}`) |
|
.join('<br/>'); |
|
tooltip.html(`<strong>${d.type}</strong><br/>${params}`) |
|
.style("left", (event.pageX + 15) + "px") |
|
.style("top", (event.pageY - 15) + "px"); |
|
} |
|
function hideTooltip() { tooltip.transition().style("opacity", 0); } |
|
|
|
function formatDocumentText(d) { |
|
const match = d.match(/Document \[([\d]+)\]\(Title:(.*?)\)(.*)/); |
|
if (match) { |
|
const number = match[1]; |
|
const title = match[2]; |
|
const content = match[3]; |
|
return `[${number}] <strong>${title}</strong>: ${content}`; |
|
} |
|
return d; |
|
} |
|
|
|
|
|
function dragstart(event, d) { |
|
if (!event.active) simulation.alphaTarget(0.3).restart(); |
|
d.fx = d.x; |
|
d.fy = d.y; |
|
} |
|
function dragging(event, d) { |
|
d.fx = event.x; |
|
d.fy = event.y; |
|
} |
|
function dragend(event, d) { |
|
if (!event.active) simulation.alphaTarget(0); |
|
d.fx = null; |
|
d.fy = null; |
|
} |
|
let allResults; |
|
|
|
|
|
fetch('res_attr.json') |
|
.then(response => response.json()) |
|
.then(data => { |
|
allResults = data; |
|
updateResult(0); |
|
}) |
|
.catch(error => { |
|
console.error('Error loading JSON file:', error); |
|
}); |
|
|
|
|
|
function highlightDocsBySentence(sentenceIndex) { |
|
|
|
const currentResult = allResults[document.getElementById("result-select").value]; |
|
|
|
currentDocValues = currentResult.doc_level[sentenceIndex]; |
|
currentSentenceIndex = sentenceIndex; |
|
|
|
if (currentGranularity === "Document-level") { |
|
const docValues = currentResult.doc_level[sentenceIndex]; |
|
|
|
const maxVal = Math.max(...docValues); |
|
const minVal = Math.min(...docValues); |
|
const range = maxVal - minVal || 1; |
|
|
|
d3.selectAll(".doc-item") |
|
.style("background", (d, j) => { |
|
const normalized = (docValues[j] - minVal) / range; |
|
return getColor(normalized); |
|
}); |
|
|
|
|
|
} else { |
|
const data = currentGranularity === "Span-level" |
|
? currentResult.span_level[sentenceIndex] |
|
: currentResult.word_level[sentenceIndex]; |
|
|
|
d3.selectAll(".doc-item").each(function(d, j) { |
|
const rawText = d.text; |
|
const formatted = formatDocumentText(rawText); |
|
const matches = rawText.match(/Document \[([\d]+)\]\(Title:(.*?)\)(.*)/); |
|
const offset = matches ? matches[0].length - matches[3].length : 0; |
|
|
|
const spans = (data[j] || []) |
|
const sorted = spans.slice().sort((a,b) => a[0] - b[0]); |
|
|
|
const values = sorted.map(x => x[1]); |
|
const minVal = Math.min(...values); |
|
const maxVal = Math.max(...values); |
|
const range = maxVal - minVal || 1; |
|
|
|
let newContent = rawText; |
|
let cumulativeShift = 0; |
|
|
|
sorted.forEach((entry, i) => { |
|
const idx = entry[0] || 0; |
|
const val = entry[1] || 0; |
|
|
|
const start = idx; |
|
const end = i < sorted.length-1 |
|
? sorted[i+1][0] |
|
: rawText.length; |
|
|
|
const color = d3.interpolateReds((val - minVal)/range); |
|
const span = `<span class="highlight-span" style="background:${color}" data-original-color="${color}" data-value="${val}" onclick="handleSpanClick(event, ${val})">`; |
|
const insertStart = start + cumulativeShift; |
|
const insertEnd = end + cumulativeShift; |
|
|
|
newContent = newContent.slice(0, insertStart) |
|
+ span |
|
+ newContent.slice(insertStart, insertEnd) |
|
+ "</span>" |
|
+ newContent.slice(insertEnd); |
|
cumulativeShift += span.length + "</span>".length; |
|
|
|
}); |
|
|
|
newContent = formatDocumentText(newContent) |
|
d3.select(this).select(".doc-content").html(newContent); |
|
setTimeout(applyThresholdFilter, 0); |
|
}); |
|
} |
|
} |
|
|
|
function updateResult(selectedIndex) { |
|
const result = allResults[selectedIndex]; |
|
|
|
currentSentenceIndex = null; |
|
currentDocValues = []; |
|
d3.select(".color-box") |
|
.style("background", "none") |
|
.style("border-color", "#ddd"); |
|
d3.select(".value-display") |
|
.text("Click document to show value") |
|
.style("color", "#666"); |
|
|
|
|
|
const docList = d3.select("#doc-list") |
|
.html("") |
|
.selectAll(".doc-item") |
|
.data(result.doc_cache.map((d, j) => ({ |
|
text: d, |
|
index: j |
|
}))) |
|
.enter() |
|
.append("div") |
|
.classed("doc-item", true) |
|
.html(d => ` |
|
<div class="doc-content">${formatDocumentText(d.text)}</div> |
|
`) |
|
.on("click", function(event, d) { |
|
if (currentGranularity === "Document-level" && currentSentenceIndex !== null) { |
|
const rawValue = currentDocValues[d.index]; |
|
const normalized = (rawValue - Math.min(...currentDocValues)) / |
|
(Math.max(...currentDocValues) - Math.min(...currentDocValues) || 1); |
|
|
|
d3.select(".color-box") |
|
.style("background", getColor(normalized)) |
|
.style("border-color", "#999"); |
|
|
|
d3.select(".value-display") |
|
.text(`Value: ${rawValue.toFixed(4)}`) |
|
.style("color", "#333"); |
|
|
|
} else { |
|
|
|
event.stopPropagation(); |
|
} |
|
}); |
|
|
|
currentThreshold = 0; |
|
d3.select("#threshold").property("value", 0); |
|
d3.select("#threshold-value").text("0.00"); |
|
|
|
|
|
d3.select("#current-question").html(`❓ ${result.data.question}`); |
|
d3.select("#current-answer").html(`📖 ${result.data.answer}`); |
|
|
|
d3.select("#output-box") |
|
.html("") |
|
.selectAll(".output-line") |
|
.data(result.output.map((d, i) => ({ text: d, index: i }))) |
|
.enter() |
|
.append("div") |
|
.classed("output-line", true) |
|
.html(d => ` |
|
<div class="output-text">${d.text}</div> |
|
`) |
|
.on("click", function(event, d) { |
|
clearDocHighlights(); |
|
if (currentGranularity === "Document-level") { |
|
highlightDocsBySentence(d.index); |
|
} else { |
|
highlightDocsBySentence(d.index); |
|
} |
|
}); |
|
|
|
} |
|
|
|
|
|
d3.select("#granularity").on("change", function() { |
|
currentGranularity = this.value; |
|
clearDocHighlights(); |
|
currentSentenceIndex = null; |
|
currentDocValues = []; |
|
applyThresholdFilter(); |
|
if (currentGranularity === "Document-level") { |
|
|
|
document.querySelector('.filter-slider').style.display = 'none'; |
|
} else { |
|
|
|
document.querySelector('.filter-slider').style.display = 'block'; |
|
} |
|
}); |
|
|
|
|
|
function clearDocHighlights() { |
|
d3.selectAll(".doc-item").style("background", "none"); |
|
d3.selectAll(".doc-content span").each(function() { |
|
const parent = this.parentNode; |
|
parent.replaceChild(document.createTextNode(this.textContent), this); |
|
parent.normalize(); |
|
d3.select(".color-box") |
|
.style("background", "none") |
|
.style("border-color", "#ddd"); |
|
d3.select(".value-display") |
|
.text("Click span to show value") |
|
.style("color", "#666"); |
|
}); |
|
} |
|
|
|
|
|
function getColor(value) { |
|
const alpha = value * 0.8 + 0.2; |
|
return `rgba(255,50,50,${alpha})`; |
|
} |
|
function parseDocumentText(text) { |
|
const match = text.match(/\[(\d+)\]\(Title:(.*?)\)(.*)/); |
|
return { |
|
number: match[1], |
|
title: match[2], |
|
content: match[3] |
|
}; |
|
} |
|
|
|
|
|
function getTextOffset(formattedHtml) { |
|
const temp = document.createElement('div'); |
|
temp.innerHTML = formattedHtml; |
|
return temp.textContent.length - temp.querySelector('strong').nextSibling.textContent.length; |
|
} |
|
</script> |
|
</body> |
|
</html> |