|
|
| <!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"}}, {"id": "verifier-[4]", "type": "verifier", "params": {"type": "verifier"}}, {"id": "gpt-3.5-turbo-[5]", "type": "Generator", "params": {"type": "Generator"}}, {"id": "retriever-[6]", "type": "retriever", "params": {"type": "retriever"}}, {"id": "gpt-3.5-turbo-[2]", "type": "Generator", "params": {"type": "Generator"}}, {"id": "simplifier-[3]", "type": "simplifier", "params": {"type": "simplifier", "model": "Llama3.1"}}]; |
| const edges = [{"source": "input", "target": "gpt-3.5-turbo-[1]", "weight": 0}, {"source": "gpt-3.5-turbo-[2]", "target": "verifier-[4]", "weight": 0}, {"source": "retriever-[6]", "target": "gpt-3.5-turbo-[2]", "weight": 0}, {"source": "gpt-3.5-turbo-[5]", "target": "retriever-[6]", "weight": 0}, {"source": "verifier-[4]", "target": "gpt-3.5-turbo-[5]", "weight": -1}, {"source": "verifier-[4]", "target": "simplifier-[3]", "weight": 1}, {"source": "simplifier-[3]", "target": "output", "weight": 0}, {"source": "gpt-3.5-turbo-[1]", "target": "verifier-[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> |