| | <!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"> |
| | |
| | <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="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: "Generating queries to retrieve evidence", |
| | 2: "Searching more articles", |
| | 3: "Scraping documents from URLs", |
| | 4: "Finding the most relevant documents", |
| | 5: "Extracting events from top documents", |
| | 6: "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 raw = statusDict?.[step] || stepList[step]; |
| | const content = raw.replace(/\n/g, "<br>"); |
| | 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}`); |
| | |
| | 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"); |
| | |
| | suggestedPledge = null; |
| | |
| | const waitForFile = setInterval(() => { |
| | if (lastUsedFile) { |
| | clearInterval(waitForFile); |
| | loadEvents(lastUsedFile); |
| | } |
| | }, 200); |
| | } else if (Object.values(data.status || {}).some(v => v.startsWith("❌"))) { |
| | 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"); |
| | |
| | if (data.length === 0) { |
| | p.innerHTML = `<div class="text-gray-500 italic"> Sorry, we do not find any progress for this pledge.</div>`; |
| | return; |
| | } |
| | |
| | |
| | p.innerHTML = |
| | 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", () => { |
| | suggestedPledge = null; |
| | clearTimeout(suggestTimer); |
| | suggestTimer = setTimeout(fetchSuggestions, 300); |
| | }); |
| | |
| | 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 this pledge? </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"); |
| | |
| | document.getElementById("status").innerHTML = ""; |
| | document.getElementById("result").classList.add("hidden"); |
| | document.getElementById("progress").classList.add("hidden"); |
| | document.getElementById("result").querySelector("p").innerHTML = ""; |
| | if (window.pollIntervalId) { |
| | clearInterval(window.pollIntervalId); |
| | window.pollIntervalId = null; |
| | } |
| | Object.keys(feedbackData).forEach(key => delete feedbackData[key]); |
| | lastUsedFile = null; |
| | lastUserId = null; |
| | lastTimestamp = null; |
| | |
| | // 🔄 可以预先显示提示 |
| | document.getElementById("result").querySelector("p").textContent = "⏳ Please wait, checking..."; |
| | document.getElementById("progress").classList.remove("hidden"); |
| | document.getElementById("result").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> |
| |
|