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> | |