Spaces:
Sleeping
Sleeping
<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}×tamp=${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> | |