Pledge_Tracker / test.html
yulongchen's picture
add
35b3f62
raw
history blame
20.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pledge Tracker – Demo</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 text-gray-800">
<header class="bg-white shadow py-4 sticky top-0 z-10">
<div class="container mx-auto flex items-center justify-between px-4">
<div class="flex items-center gap-2">
<span class="text-2xl font-bold text-purple-600">🤗</span>
<span class="font-semibold text-lg">Pledge Tracking</span>
</div>
<nav class="hidden md:flex gap-6 font-medium">
<a class="hover:text-purple-600" href="#eval-response">Track Your Pledge</a>
<a class="hover:text-purple-600" href="#about">About</a>
</nav>
</div>
</header>
<section class="py-16 bg-gradient-to-r from-purple-50 to-purple-50 text-center">
<div class="container mx-auto px-4 max-w-2xl">
<h1 class="text-3xl md:text-4xl font-extrabold mb-4">
Fact-Checking Election Promises
</h1>
<p class="text-lg text-gray-600">
Extract progress towards fulfilling the promise.
</p>
</div>
</section>
<section id="eval-response" class="py-12">
<div class="container mx-auto px-4 max-w-4xl">
<!-- <h2 class="text-2xl font-bold mb-6">Track Manifesto Pledge</h2> -->
<label for="claim" class="block text-sm font-medium mb-2">
Please enter the pledge:
</label>
<textarea
id="claim"
class="w-full border rounded-lg p-3 h-40 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="For example: 'We will support families with children by introducing free breakfast clubs in every primary school...'"
></textarea>
<div id="similar-suggestions" class="mt-3 text-sm text-gray-600 hidden"></div>
<div class="mt-4">
<label for="pledge-date" class="block text-sm font-medium mb-2">
When was this pledge made?
</label>
<div class="grid grid-cols-[1fr_auto] items-center gap-2">
<input
type="date"
id="pledge-date"
class="w-full border rounded-lg p-2"
/>
<button
onclick="setDefaultDate()"
type="button"
class="px-2 py-1 text-sm bg-purple-600 text-white rounded hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
Use default: 4th Jul 2024
</button>
</div>
<div id="date-warning" class="text-sm text-red-600 mt-1 hidden">
Please select a date or click the button to use the default.
</div>
</div>
<div class="mt-4">
<label for="pledge-author" class="block text-sm font-medium mb-2">
Who made this pledge?
</label>
<div class="grid grid-cols-[1fr_auto] items-center gap-2">
<input
type="text"
id="pledge-author"
class="w-full border rounded-lg p-2"
placeholder="Enter the name of the party or person"
/>
<button
onclick="setDefaultAuthor()"
type="button"
class="px-2 py-1 text-sm bg-purple-600 text-white rounded hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
Use default: Labour
</button>
</div>
<div id="author-warning" class="text-sm text-red-600 mt-1 hidden">
Please enter a speaker or click the button to use the default.
</div>
</div>
<label for="time-range" class="block text-sm font-medium mt-4 mb-2">
Please select a time range:
</label>
<select id="time-range" class="w-full border rounded-lg p-2">
<option value="week">Past one week</option>
<option value="month">Past one month</option>
<!-- <option value="year">From when the pledge was made</option> -->
<option value="since_pledge_date">From when the pledge was made</option>
</select>
<button
id="check"
class="mt-4 px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
Let's fact check!
</button>
<div id="progress" class="mt-6 hidden border p-4 rounded-lg bg-white shadow">
<h3 class="font-semibold mb-2">System Progress</h3>
<div id="status" class="text-sm text-gray-800 font-normal leading-relaxed"></div>
</div>
<div id="result" class="mt-6 hidden border p-4 rounded-lg bg-white shadow">
<h3 class="font-semibold mb-2">Result</h3>
<p class="text-gray-700"></p>
</div>
</div>
</section>
<section id="about" class="py-12">
<div class="container mx-auto px-4 max-w-4xl">
<h2 class="text-2xl font-bold mb-6">About</h2>
<p class="text-gray-700 leading-relaxed">
This demo connects a static front-end with a Python back-end using Flask.
The back-end generates event data and returns structured events related
to a manifesto pledge.
</p>
</div>
</section>
<script>
let suggestedPledge = null;
let currentAbortController = null;
const feedbackData = {};
let lastUsedFile = null;
let lastUserId = null;
let lastTimestamp = null;
const checkBtn = document.getElementById("check");
const stepListStandard = {
1: "Retrieving evidence related to the pledge",
2: "Scraping documents from URLs",
3: "Generating more queries based on the retrieved evidence",
4: "Searching more articles",
5: "Scraping documents from URLs",
6: "Finding the most relevant documents",
7: "Extracting events from top documents",
8: "Sorting events temporally"
};
const stepListSuggestion = {
1: "Retrieving evidence based on genertaed queries",
2: "Scraping documents from URLs",
3: "Finding the most relevant documents",
4: "Extracting events from top documents",
5: "Sorting events temporally"
};
let stepList = stepListStandard;
function renderStatus(statusDict) {
let html = "<ul class='list-disc ml-6 space-y-1 text-sm'>";
for (let step in stepList) {
const content = statusDict?.[step] || stepList[step];
const prefix = statusDict?.[step] ? "✅" : "⏳";
html += `<li>${prefix} Step ${step}: ${content}</li>`;
}
html += "</ul>";
return html;
}
function setDefaultDate() {
const input = document.getElementById("pledge-date");
input.value = "2024-07-04";
document.getElementById("date-warning").classList.add("hidden");
}
function setDefaultAuthor() {
const input = document.getElementById("pledge-author");
input.value = "Labour";
document.getElementById("author-warning").classList.add("hidden");
}
// function setFeedback(index, answer) {
// feedbackData[index] = answer;
// const message = document.getElementById(`msg-${index}`);
// message.textContent = `✓ Selected: ${answer ? 'Yes' : 'No'}`;
// message.className = answer
// ? "text-sm text-green-600 mt-1"
// : "text-sm text-red-600 mt-1";
// }
function setFeedback(index, answer) {
feedbackData[index] = answer;
const message = document.getElementById(`msg-${index}`);
let displayText = "";
let colorClass = "";
switch(answer) {
case "not_relevant":
displayText = "Not relevant";
colorClass = "text-red-300";
break;
case "relevant_seen":
displayText = "Relevant but already seen";
colorClass = "text-grey-400";
break;
case "relevant_updated":
displayText = "Relevant and up-to-date";
colorClass = "text-blue-400";
break;
}
message.textContent = `✓ Selected: ${displayText}`;
message.className = `text-sm ${colorClass} mt-1`;
}
function pollStatus(userId, timestamp, statusElement) {
if (window.pollIntervalId) {
clearInterval(window.pollIntervalId);
}
window.pollIntervalId = setInterval(async () => {
try {
const res = await fetch(`/api/status?user_id=${userId}&timestamp=${timestamp}&_=${Date.now()}`);
const data = await res.json();
// 动态渲染结构化状态
if (data.status) {
statusElement.innerHTML = renderStatus(data.status);
}
// 检查是否完成
const values = Object.values(data.status || {});
const finalText = values.join(" ").toLowerCase();
if (finalText.includes("done") || finalText.includes("finished")) {
clearInterval(window.pollIntervalId);
window.pollIntervalId = null;
statusElement.innerHTML += `<div class="mt-2 text-green-600 font-semibold">✅ All done.</div>`;
checkBtn.disabled = false;
checkBtn.classList.remove("opacity-50", "cursor-not-allowed");
if (lastUsedFile) loadEvents(lastUsedFile);
} else if (finalText.includes("error") || finalText.includes("fail")) {
clearInterval(window.pollIntervalId);
window.pollIntervalId = null;
statusElement.innerHTML += `<div class="mt-2 text-red-600 font-semibold">❌ The process failed.</div>`;
checkBtn.disabled = false;
checkBtn.classList.remove("opacity-50", "cursor-not-allowed");
}
} catch (err) {
clearInterval(window.pollIntervalId);
window.pollIntervalId = null;
statusElement.innerHTML = `<div class="text-red-600">❌ Failed to check status: ${err.message}</div>`;
}
}, 2000);
}
async function submitAllFeedback() {
const entries = Object.entries(feedbackData);
if (entries.length === 0) {
alert("No feedback to submit!");
return;
}
const confirmed = confirm("Submit all feedback?");
if (!confirmed) return;
const pledgeText = document.getElementById("claim").value.trim();
const res = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pledge: pledgeText,
file: lastUsedFile,
user_id: lastUserId,
timestamp: lastTimestamp,
feedback: entries.map(([index, answer]) => ({
eventIndex: index,
answer: answer
}))
})
});
alert(res.ok ? "✅ Feedback submitted successfully!" : "❌ Submission failed.");
}
async function loadEvents(file) {
const resultBox = document.getElementById("result");
const p = resultBox.querySelector("p");
resultBox.classList.remove("hidden");
try {
const fileParam = encodeURIComponent(file);
const eventsRes = await fetch(`/api/events?file=${fileParam}`);
if (!eventsRes.ok) throw new Error("Event file not found or malformed");
const data = await eventsRes.json();
if (!Array.isArray(data)) throw new Error("Unexpected data format");
p.innerHTML = `<strong>We have found ${data.length} events for this pledge.</strong><br><br>` +
data.map((e, index) => `
<div class="mb-6 border-b pb-4">
🗓️ <b>${e.date}</b>: ${e.event}<br>
🔗 <a href="${e.url}" target="_blank" class="text-purple-400 underline">Source</a>
<div class="mt-3">
<label class="block text-sm font-medium mb-2">How relevant is this event?</label>
<div class="flex flex-wrap gap-2">
<button onclick="setFeedback(${index}, 'not_relevant')"
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg text-gray-700">
Not relevant
</button>
<button onclick="setFeedback(${index}, 'relevant_seen')"
class="px-3 py-1.5 bg-blue-100 hover:bg-blue-200 border border-blue-300 rounded-lg text-blue-700">
Relevant but seen
</button>
<button onclick="setFeedback(${index}, 'relevant_updated')"
class="px-3 py-1.5 bg-green-100 hover:bg-green-200 border border-green-300 rounded-lg text-green-700">
Relevant & up-to-date
</button>
</div>
<div id="msg-${index}" class="text-sm mt-1"></div>
</div>
</div>
`).join('') +
`<button onclick="submitAllFeedback()" class="mt-6 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
📤 Submit All Feedback
</button>
<button onclick="window.location.href='/download?file=${fileParam}'" class="mt-4 ml-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
📅 Download Excel
</button>`;
} catch (err) {
p.textContent = `❌ Failed to load timeline: ${err.message}`;
}
}
let suggestTimer = null;
document.getElementById("claim").addEventListener("input", () => {
clearTimeout(suggestTimer);
suggestTimer = setTimeout(fetchSuggestions, 300); // 300ms delay to avoid flooding
});
async function fetchSuggestions() {
const claimText = document.getElementById("claim").value.trim();
const suggestionBox = document.getElementById("similar-suggestions");
if (!claimText) {
suggestionBox.classList.add("hidden");
return;
}
const res = await fetch("/api/similar-pledges", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claim: claimText })
});
const data = await res.json();
const suggestions = data.suggestions || [];
if (suggestions.length === 0) {
suggestionBox.classList.add("hidden");
} else {
const author = "Labour";
const date = "2024-07-04";
suggestionBox.innerHTML =
"<div class='font-semibold mb-1'>💡 Are you fact-checking ... </div>" +
"<ul class='list-disc ml-6 mt-1'>" +
suggestions.map(s => `
<li class="mb-2">
${author}: ${s.text} (${date})
<button
onclick="useSuggestedPledge('${s.text.replace(/'/g, "\\'")}', ${s.index})"
class="ml-2 px-2 py-1 text-xs bg-purple-600 text-white rounded hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500">
Fact-check this pledge
</button>
</li>
`).join("") +
"</ul>";
suggestionBox.classList.remove("hidden");
}
}
checkBtn.addEventListener("click", async () => {
const claim = document.getElementById("claim").value.trim();
const pledgeDate = document.getElementById("pledge-date").value.trim();
const pledgeAuthor = document.getElementById("pledge-author").value.trim();
const statusElement = document.getElementById("status");
const resultBox = document.getElementById("result");
// resultBox.classList.remove("hidden");
const p = resultBox.querySelector("p");
let valid = true;
if (!claim) {
alert("Please enter the pledge text.");
valid = false;
}
if (!pledgeDate) {
document.getElementById("date-warning").classList.remove("hidden");
valid = false;
}
if (!pledgeAuthor) {
document.getElementById("author-warning").classList.remove("hidden");
valid = false;
}
if (!valid) return;
checkBtn.disabled = true;
checkBtn.classList.add("opacity-50", "cursor-not-allowed");
// document.getElementById("status").classList.remove("hidden");
statusElement.innerHTML = renderStatus({});
document.getElementById("result").classList.remove("hidden");
document.getElementById("progress").classList.remove("hidden");
try {
const timeRange = document.getElementById("time-range").value;
const pledgeDate = document.getElementById("pledge-date").value;
const pledgeAuthor = document.getElementById("pledge-author").value;
if (currentAbortController) currentAbortController.abort();
currentAbortController = new AbortController();
const signal = currentAbortController.signal;
let valid = true;
stepList = (suggestedPledge !== null) ? stepListSuggestion : stepListStandard;
if (!pledgeDate) {
document.getElementById("date-warning").classList.remove("hidden");
valid = false;
}
if (!pledgeAuthor) {
document.getElementById("author-warning").classList.remove("hidden");
valid = false;
}
if (!valid) return;
const userId = Math.random().toString(36).substring(2, 10);
const now = new Date();
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
statusElement.textContent = "";
// pollStatus(userId, timestamp, p);
pollStatus(userId, timestamp, document.getElementById("status"));
const runRes = await fetch("/api/run-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
claim,
time_range: timeRange,
pledge_date: pledgeDate,
pledge_author: pledgeAuthor,
user_id: userId,
timestamp: timestamp,
signal: signal,
suggestion_meta: suggestedPledge
})
});
const runData = await runRes.json();
lastUsedFile = runData.file;
lastUserId = runData.user_id;
lastTimestamp = runData.timestamp;
} catch (err) {
if (err.name === "AbortError") {
console.log("Previous request aborted.");
checkBtn.disabled = false;
checkBtn.classList.remove("opacity-50", "cursor-not-allowed");
return;
}
p.textContent = `❌ Failed to load timeline: ${err.message}`;
}
});
async function useSuggestedPledge(text, index) {
document.getElementById("claim").value = text;
document.getElementById("pledge-author").value = "Labour";
document.getElementById("pledge-date").value = "2024-07-04";
suggestedPledge = { text, index };
alert("✅ This pledge has been filled in. You can now click 'Let's fact check!'");
await fetch("/api/log-similar-selection", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
selected_text: text,
index: index
})
});
}
</script>
</body>
</html>