function SentimentWidget(
data,
{ value = null } = {}
) {
const element = html`
<div style="padding: 24px; border: 1px solid #e0e0e0; border-radius: 12px; max-width: 600px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<textarea
placeholder="Enter professor review..."
style="width: 100%;
height: 100px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
resize: vertical;
margin-bottom: 16px;"
></textarea>
<div style="display: flex; gap: 12px; margin: 16px 0;">
<button style="
padding: 8px 16px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
">Analyze</button>
<select style="
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
min-width: 120px;
cursor: pointer;
">
<option value="all">Show All</option>
<option value="POSITIVE">Positive Only</option>
<option value="NEGATIVE">Negative Only</option>
<option value="NEUTRAL">Neutral Only</option>
</select>
</div>
<div class="loading" style="
display: none;
color: #666;
font-style: italic;
margin: 10px 0;
">Analyzing...</div>
<div class="chart" style="
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
"></div>
</div>
`;
const textarea = element.querySelector("textarea");
const button = element.querySelector("button");
const chartDiv = element.querySelector(".chart");
const loading = element.querySelector(".loading");
const filterSelect = element.querySelector("select");
button.onmouseover = () => button.style.background = '#357abd';
button.onmouseout = () => button.style.background = '#4a90e2';
async function updateSentiment() {
const text = textarea.value;
if (!text.trim()) return;
loading.style.display = "block";
chartDiv.innerHTML = "";
try {
const result = await analyzeSentiment(text);
const predictions = result.flat();
const sentimentData = {
POSITIVE: 0,
NEGATIVE: 0,
NEUTRAL: 0
};
for (const s of predictions) {
sentimentData[s.label] = s.score;
}
const sum = sentimentData.POSITIVE + sentimentData.NEGATIVE;
sentimentData.NEUTRAL = Math.max(0, 1 - sum);
widget.value = sentimentData;
showValue();
} catch (e) {
chartDiv.innerHTML = `<div style="color: #d32f2f; padding: 16px; background: #ffebee; border-radius: 8px;">
Error: ${e.message}
</div>`;
} finally {
loading.style.display = "none";
}
}
function showValue() {
chartDiv.innerHTML = "";
if (!widget.value) return;
const selected = filterSelect.value;
const filtered = Object.entries(widget.value)
.filter(([label]) => selected === "all" || selected === label)
.map(([name, val]) => ({ name, value: val }));
chartDiv.appendChild(drawPieChart(filtered));
}
function drawPieChart(data) {
const width = 400, height = 400;
const radius = Math.min(width, height) / 2 * 0.65;
const labelOffset = radius * 1.2;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const color = d3.scaleOrdinal()
.domain(data.map(d => d.name))
.range([
"#2ecc71",
"#f1c40f",
"#e74c3c"
]);
const pie = d3.pie()
.value(d => d.value)
.sort(null);
const arc = d3.arc()
.innerRadius(radius * 0.6)
.outerRadius(radius);
svg.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", d => color(d.data.name))
.attr("stroke", "white")
.style("stroke-width", "2px");
function calculateLabelPosition(d, i, data) {
const angle = (d.startAngle + d.endAngle) / 2;
const radians = angle - Math.PI / 2;
let x = Math.cos(radians) * labelOffset;
let y = Math.sin(radians) * labelOffset;
if (data.length > 1) {
const spacing = 25;
if (i > 0) {
y += spacing * i;
}
}
return { x, y, angle: radians };
}
const labelGroups = svg.selectAll(".label-group")
.data(pie(data))
.join("g")
.attr("class", "label-group");
labelGroups.append("path")
.attr("class", "label-line")
.attr("stroke", "#999")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("d", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
const midRadius = (arc.innerRadius()() + arc.outerRadius()()) / 2;
const midX = Math.cos(pos.angle) * midRadius;
const midY = Math.sin(pos.angle) * midRadius;
return `M ${midX},${midY} L ${pos.x},${pos.y}`;
});
labelGroups.append("rect")
.attr("transform", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
return `translate(${pos.x - 45},${pos.y - 10})`;
})
.attr("width", 90)
.attr("height", 20)
.attr("fill", "white")
.attr("rx", 4)
.style("opacity", 0.9);
labelGroups.append("text")
.text(d => `${d.data.name}: ${(d.data.value * 100).toFixed(1)}%`)
.attr("transform", (d, i) => {
const pos = calculateLabelPosition(d, i, data);
return `translate(${pos.x},${pos.y})`;
})
.style("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "500")
.style("fill", "#333");
const total = d3.sum(data, d => d.value);
svg.append("text")
.text(`Total`)
.attr("text-anchor", "middle")
.attr("dy", "-0.5em")
.style("font-size", "14px")
.style("font-weight", "bold");
svg.append("text")
.text(`${(total * 100).toFixed(1)}%`)
.attr("text-anchor", "middle")
.attr("dy", "1em")
.style("font-size", "14px")
.style("font-weight", "bold");
return svg.node().ownerSVGElement;
}
button.onclick = updateSentiment;
filterSelect.oninput = showValue;
const widget = ReactiveWidget(element, { value, showValue });
showValue();
return widget;
}