User:Polygnotus/Scripts/AI Source Verification.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:Polygnotus/Scripts/AI Source Verification. |
//Inspired by User:Phlsph7/SourceVerificationAIAssistant.js
(function() {
'use strict';
class WikipediaSourceVerifier {
constructor() {
this.providers = {
claude: {
name: 'Claude',
storageKey: 'claude_api_key',
color: '#0645ad',
model: 'claude-sonnet-4-20250514'
},
gemini: {
name: 'Gemini',
storageKey: 'gemini_api_key',
color: '#4285F4',
model: 'gemini-2.5-flash-preview-05-20'
},
openai: {
name: 'ChatGPT',
storageKey: 'openai_api_key',
color: '#10a37f',
model: 'gpt-4o'
}
};
this.currentProvider = localStorage.getItem('source_verifier_provider') || 'claude';
this.sidebarWidth = localStorage.getItem('verifier_sidebar_width') || '400px';
this.isVisible = localStorage.getItem('verifier_sidebar_visible') !== 'false';
this.currentResults = localStorage.getItem('verifier_current_results') || '';
this.buttons = {};
this.activeClaim = null;
this.activeSource = null;
this.init();
}
init() {
this.loadOOUI().then(() => {
this.createUI();
this.attachEventListeners();
this.attachReferenceClickHandlers();
this.adjustMainContent();
});
}
async loadOOUI() {
await mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows']);
}
getCurrentApiKey() {
return localStorage.getItem(this.providers[this.currentProvider].storageKey);
}
setCurrentApiKey(key) {
localStorage.setItem(this.providers[this.currentProvider].storageKey, key);
}
removeCurrentApiKey() {
localStorage.removeItem(this.providers[this.currentProvider].storageKey);
}
getCurrentColor() {
return this.providers[this.currentProvider].color;
}
createUI() {
const sidebar = document.createElement('div');
sidebar.id = 'source-verifier-sidebar';
this.createOOUIButtons();
sidebar.innerHTML = `
<div id="verifier-sidebar-header">
<h3>Source Verifier</h3>
<div id="verifier-sidebar-controls">
<div id="verifier-close-btn-container"></div>
</div>
</div>
<div id="verifier-sidebar-content">
<div id="verifier-controls">
<div id="verifier-provider-container"></div>
<div id="verifier-buttons-container"></div>
</div>
<div id="verifier-claim-section">
<h4>Selected Claim</h4>
<div id="verifier-claim-text">Click on a reference number [1] next to a claim to verify it against its source.</div>
</div>
<div id="verifier-source-section">
<h4>Source Content</h4>
<div id="verifier-source-text">No source loaded yet.</div>
</div>
<div id="verifier-results">
<h4>Verification Result</h4>
<div id="verifier-output">${this.currentResults}</div>
</div>
</div>
<div id="verifier-resize-handle"></div>
`;
this.createVerifierTab();
this.createStyles();
document.body.append(sidebar);
this.appendOOUIButtons();
if (!this.isVisible) {
this.hideSidebar();
}
this.makeResizable();
}
createStyles() {
const style = document.createElement('style');
style.textContent = `
#source-verifier-sidebar {
position: fixed;
top: 0;
right: 0;
width: ${this.sidebarWidth};
height: 100vh;
background: #fff;
border-left: 2px solid ${this.getCurrentColor()};
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
#verifier-sidebar-header {
background: ${this.getCurrentColor()};
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
#verifier-sidebar-header h3 {
margin: 0;
font-size: 16px;
}
#verifier-sidebar-controls {
display: flex;
gap: 8px;
}
#verifier-sidebar-content {
padding: 15px;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
}
#verifier-controls {
flex-shrink: 0;
}
#verifier-provider-container {
margin-bottom: 10px;
}
#verifier-buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
}
#verifier-buttons-container .oo-ui-buttonElement {
width: 100%;
}
#verifier-buttons-container .oo-ui-buttonElement-button {
width: 100%;
justify-content: center;
}
#verifier-claim-section, #verifier-source-section, #verifier-results {
flex-shrink: 0;
}
#verifier-claim-section h4, #verifier-source-section h4, #verifier-results h4 {
margin: 0 0 8px 0;
color: ${this.getCurrentColor()};
font-size: 14px;
font-weight: bold;
}
#verifier-claim-text, #verifier-source-text {
padding: 10px;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
max-height: 120px;
overflow-y: auto;
}
#verifier-output {
padding: 10px;
background: #fafafa;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
max-height: 480px;
overflow-y: auto;
white-space: pre-wrap;
}
#verifier-output h1, #verifier-output h2, #verifier-output h3 {
color: ${this.getCurrentColor()};
margin-top: 16px;
margin-bottom: 8px;
}
#verifier-output h1 { font-size: 1.3em; }
#verifier-output h2 { font-size: 1.2em; }
#verifier-output h3 { font-size: 1.1em; }
#verifier-output ul, #verifier-output ol {
padding-left: 18px;
}
#verifier-output p {
margin-bottom: 10px;
}
#verifier-output strong {
color: #d33;
}
#verifier-resize-handle {
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: transparent;
cursor: ew-resize;
z-index: 10001;
}
#verifier-resize-handle:hover {
background: ${this.getCurrentColor()};
opacity: 0.5;
}
#ca-verifier {
display: none;
}
#ca-verifier a {
color: ${this.getCurrentColor()} !important;
text-decoration: none !important;
padding: 0.5em !important;
}
#ca-verifier a:hover {
text-decoration: underline !important;
}
body {
margin-right: ${this.isVisible ? this.sidebarWidth : '0'};
transition: margin-right 0.3s ease;
}
.verifier-error {
color: #d33;
background: #fef2f2;
border: 1px solid #fecaca;
padding: 8px;
border-radius: 4px;
}
.verifier-sidebar-hidden body {
margin-right: 0 !important;
}
.verifier-sidebar-hidden #source-verifier-sidebar {
display: none;
}
.verifier-sidebar-hidden #ca-verifier {
display: list-item !important;
}
.reference:hover {
background-color: #e6f3ff;
cursor: pointer;
}
.reference.verifier-active {
background-color: ${this.getCurrentColor()};
color: white;
}
.claim-highlight {
background-color: #fff3cd;
border-left: 3px solid ${this.getCurrentColor()};
padding-left: 5px;
margin-left: -8px;
}
`;
document.head.appendChild(style);
}
createOOUIButtons() {
this.buttons.close = new OO.ui.ButtonWidget({
icon: 'close',
title: 'Close',
framed: false,
classes: ['verifier-close-button']
});
// Provider selector
this.buttons.providerSelect = new OO.ui.DropdownWidget({
menu: {
items: Object.keys(this.providers).map(key =>
new OO.ui.MenuOptionWidget({
data: key,
label: this.providers[key].name
})
)
}
});
this.buttons.providerSelect.getMenu().selectItemByData(this.currentProvider);
this.buttons.setKey = new OO.ui.ButtonWidget({
label: 'Set API Key',
flags: ['primary', 'progressive'],
disabled: false
});
this.buttons.verify = new OO.ui.ButtonWidget({
label: 'Verify Claim',
flags: ['primary', 'progressive'],
icon: 'check',
disabled: true
});
this.buttons.changeKey = new OO.ui.ButtonWidget({
label: 'Change Key',
flags: ['safe'],
icon: 'edit',
disabled: false
});
this.buttons.removeKey = new OO.ui.ButtonWidget({
label: 'Remove API Key',
flags: ['destructive'],
icon: 'trash',
disabled: false
});
this.updateButtonVisibility();
}
appendOOUIButtons() {
document.getElementById('verifier-close-btn-container').appendChild(this.buttons.close.$element[0]);
document.getElementById('verifier-provider-container').appendChild(this.buttons.providerSelect.$element[0]);
const container = document.getElementById('verifier-buttons-container');
if (this.getCurrentApiKey()) {
container.appendChild(this.buttons.verify.$element[0]);
container.appendChild(this.buttons.changeKey.$element[0]);
container.appendChild(this.buttons.removeKey.$element[0]);
} else {
container.appendChild(this.buttons.setKey.$element[0]);
}
}
updateButtonVisibility() {
const container = document.getElementById('verifier-buttons-container');
if (!container) return;
container.innerHTML = '';
if (this.getCurrentApiKey()) {
// Enable verify button if we have API key and either claim+source OR just show it enabled
const hasClaimAndSource = this.activeClaim && this.activeSource;
this.buttons.verify.setDisabled(!hasClaimAndSource);
container.appendChild(this.buttons.verify.$element[0]);
container.appendChild(this.buttons.changeKey.$element[0]);
container.appendChild(this.buttons.removeKey.$element[0]);
// Debug logging
console.log('Button visibility update:', {
hasApiKey: !!this.getCurrentApiKey(),
activeClaim: !!this.activeClaim,
activeSource: !!this.activeSource,
buttonDisabled: !hasClaimAndSource
});
} else {
this.buttons.verify.setDisabled(true);
container.appendChild(this.buttons.setKey.$element[0]);
}
}
createVerifierTab() {
if (typeof mw !== 'undefined' && mw.config.get('wgNamespaceNumber') === 0) {
let portletId = 'p-namespaces';
if (mw.config.get('skin') === 'vector-2022') {
portletId = 'p-associated-pages';
}
const verifierLink = mw.util.addPortletLink(
portletId,
'#',
'Verify',
't-verifier',
'Verify claims against sources',
'v',
);
verifierLink.addEventListener('click', (e) => {
e.preventDefault();
this.showSidebar();
});
}
}
attachReferenceClickHandlers() {
// Attach click handlers to all reference links
const references = document.querySelectorAll('.reference a');
references.forEach(ref => {
ref.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleReferenceClick(ref);
});
});
}
async handleReferenceClick(refElement) {
try {
// Clear previous highlights
this.clearHighlights();
// Show sidebar if hidden
this.showSidebar();
// Extract claim text (sentence before the reference)
const claim = this.extractClaimText(refElement);
if (!claim) {
this.updateStatus('Could not extract claim text', true);
return;
}
// Highlight the claim in the article
this.highlightClaim(refElement, claim);
// Mark this reference as active
refElement.parentElement.classList.add('verifier-active');
// Extract reference URL
const refUrl = this.extractReferenceUrl(refElement);
if (!refUrl) {
this.updateStatus('Could not extract reference URL', true);
return;
}
// Update UI with claim
this.activeClaim = claim;
document.getElementById('verifier-claim-text').textContent = claim;
// Fetch source content (URL extraction)
this.updateStatus('Extracting source URL...');
const sourceInfo = await this.fetchSourceContent(refUrl);
if (!sourceInfo) {
this.updateStatus('Could not extract source URL', true);
return;
}
// Update UI with source info
this.activeSource = sourceInfo;
const sourceElement = document.getElementById('verifier-source-text');
// Show the URL and indicate content will be fetched by AI
const urlMatch = sourceInfo.match(/Source URL: (https?:\/\/[^\s\n]+)/);
if (urlMatch) {
sourceElement.innerHTML = `<strong>Source URL:</strong><br><a href="${urlMatch[1]}" target="_blank" style="word-break: break-all;">${urlMatch[1]}</a><br><br><em>Content will be fetched and analyzed by AI during verification.</em>`;
} else {
sourceElement.textContent = sourceInfo;
}
// Enable verify button now that we have both claim and source
this.updateButtonVisibility();
this.updateStatus('Ready to verify claim against source');
console.log('Reference click completed:', {
claim: this.activeClaim ? 'Set' : 'Missing',
source: this.activeSource ? 'Set' : 'Missing',
apiKey: this.getCurrentApiKey() ? 'Set' : 'Missing'
});
} catch (error) {
console.error('Error handling reference click:', error);
this.updateStatus(`Error: ${error.message}`, true);
}
}
extractClaimText(refElement) {
// Get the paragraph or container element
const container = refElement.closest('p, li, td, div, section');
if (!container) {
return '';
}
// Get all text content from the container
let fullText = '';
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
null,
false
);
let textNode;
const textNodes = [];
while (textNode = walker.nextNode()) {
textNodes.push({
node: textNode,
text: textNode.textContent,
offset: fullText.length
});
fullText += textNode.textContent;
}
// Find the position of the reference in the full text
const refText = refElement.textContent; // This should be like "[1]"
let refPosition = -1;
// Look for the reference pattern in the text
const refPattern = new RegExp('\\[\\s*' + refText.replace(/[\[\]]/g, '') + '\\s*\\]');
const refMatch = fullText.match(refPattern);
if (refMatch) {
refPosition = refMatch.index;
} else {
// Fallback: find any reference pattern before our element
const allRefs = fullText.match(/\[\d+\]/g);
if (allRefs) {
// Find the last reference before the end
let lastRefIndex = -1;
let searchPos = 0;
for (const ref of allRefs) {
const index = fullText.indexOf(ref, searchPos);
if (index !== -1) {
lastRefIndex = index;
searchPos = index + ref.length;
}
}
refPosition = lastRefIndex;
}
}
if (refPosition === -1) {
refPosition = fullText.length;
}
// Extract text before the reference
const textBeforeRef = fullText.substring(0, refPosition).trim();
// Find the last complete sentence
const sentences = textBeforeRef.split(/([.!?]+\s+)/);
if (sentences.length === 1) {
// No sentence breaks found, return the entire text
return textBeforeRef;
}
// Reconstruct the last sentence
let lastSentence = '';
let foundSentenceEnd = false;
// Work backwards through the sentence fragments
for (let i = sentences.length - 1; i >= 0; i--) {
const fragment = sentences[i];
if (fragment.match(/^[.!?]+\s*$/)) {
// This is punctuation + whitespace
if (!foundSentenceEnd) {
foundSentenceEnd = true;
continue;
} else {
// We've found the end of the previous sentence
break;
}
} else {
// This is text content
lastSentence = fragment + lastSentence;
if (foundSentenceEnd) {
// We have a complete sentence
break;
}
}
}
// If we didn't find a proper sentence boundary, fall back to a more aggressive approach
if (!lastSentence.trim() || lastSentence.trim().length < 10) {
// Look for other potential sentence boundaries
const alternativeBreaks = textBeforeRef.split(/([.!?:;]\s+|^\s*[A-Z])/);
if (alternativeBreaks.length > 1) {
lastSentence = alternativeBreaks[alternativeBreaks.length - 1];
} else {
// As a last resort, take a reasonable chunk from the end
const words = textBeforeRef.split(/\s+/);
if (words.length > 15) {
lastSentence = words.slice(-15).join(' ');
} else {
lastSentence = textBeforeRef;
}
}
}
// Clean up the result
lastSentence = lastSentence.trim();
// Ensure it ends with proper punctuation for context
if (lastSentence && !lastSentence.match(/[.!?]$/)) {
// Check if the original text continues with punctuation
const nextChar = fullText.charAt(refPosition - lastSentence.length - 1);
if (nextChar.match(/[.!?]/)) {
lastSentence = nextChar + ' ' + lastSentence;
}
}
// Remove any remaining reference brackets that might have been included
lastSentence = lastSentence.replace(/\[\d+\]/g, '').trim();
return lastSentence;
}
extractReferenceUrl(refElement) {
// Get the reference ID from the href
const href = refElement.getAttribute('href');
if (!href || !href.startsWith('#')) {
return null;
}
const refId = href.substring(1);
const refTarget = document.getElementById(refId);
if (!refTarget) {
return null;
}
// Look for URLs in the reference
const links = refTarget.querySelectorAll('a[href^="http"]');
if (links.length === 0) {
return null;
}
// Return the first external URL found
return links[0].href;
}
async fetchSourceContent(url) {
// Instead of trying to fetch the content directly (which fails due to CORS),
// we'll use the AI API to fetch and analyze the content
try {
// For now, return the URL - the AI will fetch it during verification
return `Source URL: ${url}\n\n[Content will be fetched by AI during verification]`;
} catch (error) {
console.error('Error with source URL:', error);
throw new Error(`Invalid source URL: ${error.message}`);
}
}
highlightClaim(refElement, claim) {
const parentElement = refElement.closest('p, li, td, div');
if (parentElement && !parentElement.classList.contains('claim-highlight')) {
parentElement.classList.add('claim-highlight');
}
}
clearHighlights() {
// Remove active reference highlighting
document.querySelectorAll('.reference.verifier-active').forEach(el => {
el.classList.remove('verifier-active');
});
// Remove claim highlighting
document.querySelectorAll('.claim-highlight').forEach(el => {
el.classList.remove('claim-highlight');
});
}
makeResizable() {
const handle = document.getElementById('verifier-resize-handle');
const sidebar = document.getElementById('source-verifier-sidebar');
if (!handle || !sidebar) return;
let isResizing = false;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isResizing) return;
const newWidth = window.innerWidth - e.clientX;
const minWidth = 300;
const maxWidth = window.innerWidth * 0.8;
if (newWidth >= minWidth && newWidth <= maxWidth) {
const widthPx = newWidth + 'px';
sidebar.style.width = widthPx;
document.body.style.marginRight = widthPx;
this.sidebarWidth = widthPx;
localStorage.setItem('verifier_sidebar_width', widthPx);
}
};
const handleMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
showSidebar() {
const verifierTab = document.getElementById('ca-verifier');
document.body.classList.remove('verifier-sidebar-hidden');
if (verifierTab) verifierTab.style.display = 'none';
document.body.style.marginRight = this.sidebarWidth;
this.isVisible = true;
localStorage.setItem('verifier_sidebar_visible', 'true');
}
hideSidebar() {
const verifierTab = document.getElementById('ca-verifier');
document.body.classList.add('verifier-sidebar-hidden');
if (verifierTab) verifierTab.style.display = 'list-item';
document.body.style.marginRight = '0';
this.clearHighlights();
this.isVisible = false;
localStorage.setItem('verifier_sidebar_visible', 'false');
}
adjustMainContent() {
if (this.isVisible) {
document.body.style.marginRight = this.sidebarWidth;
} else {
document.body.style.marginRight = '0';
}
}
attachEventListeners() {
this.buttons.close.on('click', () => {
this.hideSidebar();
});
this.buttons.providerSelect.getMenu().on('select', (item) => {
this.currentProvider = item.getData();
localStorage.setItem('source_verifier_provider', this.currentProvider);
this.updateButtonVisibility();
this.updateTheme();
this.updateStatus(`Switched to ${this.providers[this.currentProvider].name}`);
});
this.buttons.setKey.on('click', () => {
this.setApiKey();
});
this.buttons.changeKey.on('click', () => {
this.setApiKey();
});
this.buttons.verify.on('click', () => {
this.verifyClaim();
});
this.buttons.removeKey.on('click', () => {
this.removeApiKey();
});
}
updateTheme() {
const color = this.getCurrentColor();
// Update theme colors similar to the original implementation
// This would update sidebar border, header background, etc.
}
setApiKey() {
const provider = this.providers[this.currentProvider];
const dialog = new OO.ui.MessageDialog();
const textInput = new OO.ui.TextInputWidget({
placeholder: `Enter your ${provider.name} API Key...`,
type: 'password',
value: this.getCurrentApiKey() || ''
});
const windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog, {
title: `Set ${provider.name} API Key`,
message: $('<div>').append(
$('<p>').text(`Enter your ${provider.name} API Key to enable source verification:`),
textInput.$element
),
actions: [
{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
action: 'cancel',
label: 'Cancel',
flags: ['safe']
}
]
}).closed.then((data) => {
if (data && data.action === 'save') {
const key = textInput.getValue().trim();
if (key) {
this.setCurrentApiKey(key);
this.updateButtonVisibility();
this.updateStatus('API key set successfully!');
// If we already have claim and source, enable verify button
if (this.activeClaim && this.activeSource) {
console.log('API key set - enabling verification');
this.updateButtonVisibility();
}
}
}
windowManager.destroy();
});
}
removeApiKey() {
OO.ui.confirm('Are you sure you want to remove the stored API key?').done((confirmed) => {
if (confirmed) {
this.removeCurrentApiKey();
this.updateButtonVisibility();
this.updateStatus('API key removed successfully!');
}
});
}
updateStatus(message, isError = false) {
// For now, we'll update the claim section to show status
// In a full implementation, you might want a dedicated status area
if (isError) {
console.error('Verifier Error:', message);
} else {
console.log('Verifier Status:', message);
}
}
async verifyClaim() {
if (!this.getCurrentApiKey() || !this.activeClaim || !this.activeSource) {
this.updateStatus('Missing API key, claim, or source content', true);
return;
}
try {
this.buttons.verify.setDisabled(true);
this.updateStatus('Verifying claim against source...');
const provider = this.providers[this.currentProvider];
let result;
switch (this.currentProvider) {
case 'claude':
result = await this.callClaudeAPI(this.activeClaim, this.activeSource);
break;
case 'gemini':
result = await this.callGeminiAPI(this.activeClaim, this.activeSource);
break;
case 'openai':
result = await this.callOpenAIAPI(this.activeClaim, this.activeSource);
break;
}
this.updateStatus('Verification complete!');
this.updateOutput(result, true);
} catch (error) {
console.error('Verification error:', error);
this.updateStatus(`Error: ${error.message}`, true);
} finally {
this.buttons.verify.setDisabled(false);
}
}
async callClaudeAPI(claim, sourceInfo) {
// Extract URL from sourceInfo
const urlMatch = sourceInfo.match(/Source URL: (https?:\/\/[^\s\n]+)/);
const sourceUrl = urlMatch ? urlMatch[1] : null;
const requestBody = {
model: this.providers.claude.model,
max_tokens: 3000,
system: `You are a fact-checking assistant. Your task is to verify whether a claim from a Wikipedia article is supported by the source content at the provided URL.
Instructions:
1. First, fetch and read the content from the provided URL
2. Analyze the claim and determine what specific facts it asserts
3. Search through the source content for information that supports or contradicts the claim
4. Provide a clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
5. Quote the specific sentences from the source that support your verdict
6. Explain any discrepancies or missing information
Be precise and objective in your analysis.`,
messages: [{
role: "user",
content: sourceUrl ?
`Please fetch the content from this URL and verify the claim against it.
Claim to verify: "${claim}"
Source URL: ${sourceUrl}` :
`Claim to verify: "${claim}"
Source content: "${sourceInfo}"`
}]
};
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.getCurrentApiKey(),
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed (${response.status}): ${errorText}`);
}
const data = await response.json();
return data.content[0].text;
}
async callGeminiAPI(claim, sourceInfo) {
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${this.providers.gemini.model}:generateContent?key=${this.getCurrentApiKey()}`;
// Extract URL from sourceInfo
const urlMatch = sourceInfo.match(/Source URL: (https?:\/\/[^\s\n]+)/);
const sourceUrl = urlMatch ? urlMatch[1] : null;
const systemPrompt = `You are a fact-checking assistant. Verify whether a Wikipedia claim is supported by the source content at the provided URL.
Instructions:
1. Fetch and read the content from the provided URL
2. Analyze the claim and determine what specific facts it asserts
3. Search through the source content for information that supports or contradicts the claim
4. Provide a clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
5. Quote the specific sentences from the source that support your verdict
6. Explain any discrepancies or missing information
Be precise and objective in your analysis.`;
const requestBody = {
contents: [{
parts: [{ "text": sourceUrl ?
`Please fetch the content from this URL and verify the claim against it.
Claim to verify: "${claim}"
Source URL: ${sourceUrl}` :
`Claim to verify: "${claim}"
Source content: "${sourceInfo}"` }],
}],
systemInstruction: {
parts: [{ "text": systemPrompt }]
},
generationConfig: {
maxOutputTokens: 2048,
temperature: 0.0,
},
tools: [
{urlContext: {}},
{googleSearch: {}},
],
};
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
const responseData = await response.json();
if (!response.ok) {
const errorDetail = responseData.error ? responseData.error.message : response.statusText;
throw new Error(`API request failed (${response.status}): ${errorDetail}`);
}
if (!responseData.candidates || !responseData.candidates[0] ||
!responseData.candidates[0].content || !responseData.candidates[0].content.parts ||
!responseData.candidates[0].content.parts[0] || !responseData.candidates[0].content.parts[0].text) {
throw new Error('Invalid API response format or no content generated.');
}
return responseData.candidates[0].content.parts[0].text;
}
async callOpenAIAPI(claim, sourceInfo) {
// Extract URL from sourceInfo
const urlMatch = sourceInfo.match(/Source URL: (https?:\/\/[^\s\n]+)/);
const sourceUrl = urlMatch ? urlMatch[1] : null;
const requestBody = {
model: this.providers.openai.model,
max_tokens: 2000,
messages: [
{
role: "system",
content: `You are a fact-checking assistant. Your task is to verify whether a claim from a Wikipedia article is supported by the source content.
Instructions:
1. Analyze the claim and determine what specific facts it asserts
2. Examine the source information provided
3. Provide a clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
4. Explain your reasoning based on the available information
5. Note any limitations due to inability to access the full source content
Be precise and objective in your analysis.`
},
{
role: "user",
content: sourceUrl ?
`I need to verify this claim against a source, but I can only provide the URL since direct content fetching isn't available.
Claim to verify: "${claim}"
Source URL: ${sourceUrl}
Please provide analysis based on what you can determine from the URL and any known information about the source. Note that full verification would require accessing the complete source content.` :
`Claim to verify: "${claim}"
Source information: "${sourceInfo}"`
}
],
temperature: 0.1
};
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getCurrentApiKey()}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error?.message || errorText;
} catch {
errorMessage = errorText;
}
throw new Error(`API request failed (${response.status}): ${errorMessage}`);
}
const data = await response.json();
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
throw new Error('Invalid API response format');
}
return data.choices[0].message.content;
}
updateOutput(content, isMarkdown = false) {
const outputEl = document.getElementById('verifier-output');
let processedContent = content;
if (isMarkdown) {
processedContent = this.markdownToHtml(content);
outputEl.innerHTML = processedContent;
} else {
outputEl.textContent = content;
}
if (content) {
this.currentResults = processedContent;
localStorage.setItem('verifier_current_results', this.currentResults);
} else {
this.currentResults = '';
localStorage.removeItem('verifier_current_results');
}
}
markdownToHtml(markdown) {
let html = markdown;
// Convert headers
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
// Convert bold and italic
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
// Convert lists
html = html.replace(/^\s*[\*\-] (.*$)/gim, '<li>$1</li>');
html = html.replace(/^\s*\d+\. (.*$)/gim, '<li>$1</li>');
// Wrap consecutive list items in ul tags
html = html.replace(/((<li>.*<\/li>\s*)+)/g, (match, p1) => {
return `<ul>${p1.replace(/\s*<li>/g,'<li>')}</ul>`;
});
// Convert paragraphs
html = html.split(/\n\s*\n/).map(paragraph => {
paragraph = paragraph.trim();
if (!paragraph) return '';
if (paragraph.startsWith('<h') || paragraph.startsWith('<ul') || paragraph.startsWith('<ol') || paragraph.startsWith('<li')) {
return paragraph;
}
return `<p>${paragraph.replace(/\n/g, '<br>')}</p>`;
}).join('');
// Clean up
html = html.replace(/<p>\s*(<(?:ul|ol|h[1-6])[^>]*>[\s\S]*?<\/(?:ul|ol|h[1-6])>)\s*<\/p>/gi, '$1');
html = html.replace(/<p>\s*<\/p>/gi, '');
return html;
}
}
// Initialize the source verifier when MediaWiki is ready
if (typeof mw !== 'undefined' && mw.config.get('wgNamespaceNumber') === 0) {
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows']).then(function() {
$(function() {
new WikipediaSourceVerifier();
});
});
}
})();