|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>WRAITHPATH • Ideological Fingerprinting</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.gradient-text { |
|
background: linear-gradient(90deg, #8b5cf6 0%, #ec4899 100%); |
|
-webkit-background-clip: text; |
|
background-clip: text; |
|
color: transparent; |
|
} |
|
.timeline-item::before { |
|
content: ''; |
|
position: absolute; |
|
left: -20px; |
|
top: 0; |
|
width: 2px; |
|
height: 100%; |
|
background: #4b5563; |
|
} |
|
.fade-in { |
|
animation: fadeIn 0.5s ease-in-out; |
|
} |
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
.pulse { |
|
animation: pulse 2s infinite; |
|
} |
|
@keyframes pulse { |
|
0% { opacity: 0.6; } |
|
50% { opacity: 1; } |
|
100% { opacity: 0.6; } |
|
} |
|
.sentiment-bar { |
|
transition: width 1s ease-in-out; |
|
} |
|
#sentimentChart { |
|
min-height: 300px; |
|
} |
|
</style> |
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/builds/compromise.min.js"></script> |
|
</head> |
|
<body class="bg-gray-900 text-gray-200 min-h-screen font-sans"> |
|
<div class="container mx-auto px-4 py-8 max-w-6xl"> |
|
|
|
<header class="mb-12"> |
|
<div class="flex justify-between items-center mb-6"> |
|
<h1 class="text-4xl font-bold"> |
|
<span class="gradient-text">WRAITHPATH</span> |
|
</h1> |
|
<button class="bg-gray-800 hover:bg-gray-700 px-4 py-2 rounded-lg flex items-center"> |
|
<i class="fas fa-user-secret mr-2"></i> |
|
<span>Login</span> |
|
</button> |
|
</div> |
|
<p class="text-xl text-gray-400 mb-6">"Their posts were a breadcrumb trail to who they really are."</p> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 shadow-lg mb-8"> |
|
<div class="flex flex-col md:flex-row gap-4"> |
|
<div class="flex-1 relative"> |
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
|
<i class="fas fa-search text-gray-500"></i> |
|
</div> |
|
<input |
|
type="text" |
|
id="profileUrl" |
|
class="w-full bg-gray-700 border border-gray-600 rounded-lg pl-10 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500" |
|
placeholder="Enter Reddit username (e.g., u/spez or just spez)" |
|
value="u/spez" |
|
> |
|
</div> |
|
<button |
|
id="analyzeBtn" |
|
class="bg-gradient-to-r from-purple-600 to-pink-500 hover:from-purple-700 hover:to-pink-600 text-white font-medium rounded-lg px-6 py-3 transition-all duration-300 flex items-center justify-center" |
|
> |
|
<i class="fas fa-fingerprint mr-2"></i> |
|
Analyze Ideology |
|
</button> |
|
</div> |
|
<div class="mt-4 flex flex-wrap gap-2"> |
|
<span class="text-xs bg-gray-700 px-2 py-1 rounded">Reddit: u/username or username</span> |
|
<span class="text-xs bg-gray-700 px-2 py-1 rounded">Coming soon: Twitter, Tumblr</span> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<section class="mb-16"> |
|
<h2 class="text-2xl font-semibold mb-6 flex items-center"> |
|
<i class="fas fa-ghost mr-3 text-purple-500"></i> |
|
How It Works |
|
</h2> |
|
<div class="grid md:grid-cols-3 gap-6"> |
|
<div class="bg-gray-800 p-6 rounded-xl hover:bg-gray-750 transition-all"> |
|
<div class="w-12 h-12 bg-purple-900 rounded-lg flex items-center justify-center mb-4"> |
|
<i class="fas fa-link text-purple-400 text-xl"></i> |
|
</div> |
|
<h3 class="text-lg font-medium mb-2">1. Input Profile</h3> |
|
<p class="text-gray-400">Provide any public social media username. We'll analyze their posting history.</p> |
|
</div> |
|
<div class="bg-gray-800 p-6 rounded-xl hover:bg-gray-750 transition-all"> |
|
<div class="w-12 h-12 bg-pink-900 rounded-lg flex items-center justify-center mb-4"> |
|
<i class="fas fa-brain text-pink-400 text-xl"></i> |
|
</div> |
|
<h3 class="text-lg font-medium mb-2">2. Cognitive Analysis</h3> |
|
<p class="text-gray-400">Our models detect linguistic patterns, sentiment shifts, and ideological markers.</p> |
|
</div> |
|
<div class="bg-gray-800 p-6 rounded-xl hover:bg-gray-750 transition-all"> |
|
<div class="w-12 h-12 bg-indigo-900 rounded-lg flex items-center justify-center mb-4"> |
|
<i class="fas fa-chart-line text-indigo-400 text-xl"></i> |
|
</div> |
|
<h3 class="text-lg font-medium mb-2">3. Get The Report</h3> |
|
<p class="text-gray-400">Receive a detailed timeline of mental state evolution and belief system changes.</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="resultsSection" class="hidden mb-16"> |
|
<div class="flex justify-between items-center mb-6"> |
|
<h2 class="text-2xl font-semibold flex items-center"> |
|
<i class="fas fa-chart-bar mr-3 text-pink-500"></i> |
|
Ideological Fingerprint |
|
</h2> |
|
<button id="downloadReport" class="bg-gray-800 hover:bg-gray-700 px-4 py-2 rounded-lg flex items-center"> |
|
<i class="fas fa-file-pdf mr-2 text-red-400"></i> |
|
<span>Download PDF</span> |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 mb-8"> |
|
<div class="flex flex-col md:flex-row gap-6"> |
|
<div class="flex-shrink-0"> |
|
<div id="avatarContainer" class="w-24 h-24 rounded-full bg-gradient-to-br from-purple-600 to-pink-500 flex items-center justify-center text-4xl font-bold"> |
|
<span id="avatarInitial">?</span> |
|
</div> |
|
</div> |
|
<div class="flex-1"> |
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4"> |
|
<div> |
|
<h3 id="profileName" class="text-xl font-bold">Unknown Profile</h3> |
|
<p id="profileHandle" class="text-gray-400">@unknown</p> |
|
</div> |
|
<div class="mt-2 md:mt-0"> |
|
<span id="profilePlatform" class="bg-gray-700 px-3 py-1 rounded-full text-sm">Reddit</span> |
|
</div> |
|
</div> |
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> |
|
<div> |
|
<p class="text-gray-500 text-sm">Posts Analyzed</p> |
|
<p id="postCount" class="text-lg font-medium">0</p> |
|
</div> |
|
<div> |
|
<p class="text-gray-500 text-sm">Time Span</p> |
|
<p id="timeSpan" class="text-lg font-medium">0 years</p> |
|
</div> |
|
<div> |
|
<p class="text-gray-500 text-sm">Ideology Score</p> |
|
<p id="ideologyScore" class="text-lg font-medium">0/100</p> |
|
</div> |
|
<div> |
|
<p class="text-gray-500 text-sm">Volatility</p> |
|
<p id="volatility" class="text-lg font-medium">Low</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 mb-8"> |
|
<h3 class="text-lg font-medium mb-4 flex items-center"> |
|
<i class="fas fa-history mr-2 text-purple-400"></i> |
|
Ideological Timeline |
|
</h3> |
|
<div class="relative pl-8"> |
|
<div id="timelineContainer"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 mb-8"> |
|
<h3 class="text-lg font-medium mb-4 flex items-center"> |
|
<i class="fas fa-brain mr-2 text-pink-400"></i> |
|
Sentiment Evolution |
|
</h3> |
|
<div class="h-64 bg-gray-700 rounded-lg"> |
|
<canvas id="sentimentChart"></canvas> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 mb-8"> |
|
<h3 class="text-lg font-medium mb-4 flex items-center"> |
|
<i class="fas fa-cloud mr-2 text-indigo-400"></i> |
|
Topic Cloud |
|
</h3> |
|
<div class="flex flex-wrap gap-3" id="topicCloud"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6"> |
|
<h3 class="text-lg font-medium mb-4 flex items-center"> |
|
<i class="fas fa-fire mr-2 text-red-400"></i> |
|
Most Controversial Posts |
|
</h3> |
|
<div id="controversialPosts" class="space-y-4"> |
|
|
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<div id="loadingState" class="hidden fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50"> |
|
<div class="text-center"> |
|
<div class="w-24 h-24 border-4 border-purple-500 border-t-pink-500 rounded-full animate-spin mb-6 mx-auto"></div> |
|
<h3 class="text-2xl font-medium mb-2">Building Cognitive Profile</h3> |
|
<p class="text-gray-400 max-w-md mx-auto">Analyzing linguistic patterns, sentiment shifts, and ideological markers across <span id="loadingPostCount">0</span> posts...</p> |
|
<div class="mt-6 bg-gray-800 rounded-full h-2 w-64 mx-auto overflow-hidden"> |
|
<div id="progressBar" class="bg-gradient-to-r from-purple-500 to-pink-500 h-full" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<footer class="mt-16 pt-8 border-t border-gray-800 text-gray-500"> |
|
<div class="flex flex-col md:flex-row justify-between items-center"> |
|
<div class="mb-4 md:mb-0"> |
|
<p>© 2023 WRAITHPATH • Ideological Fingerprinting</p> |
|
</div> |
|
<div class="flex space-x-6"> |
|
<a href="#" class="hover:text-gray-300">Privacy</a> |
|
<a href="#" class="hover:text-gray-300">Terms</a> |
|
<a href="#" class="hover:text-gray-300">API</a> |
|
<a href="#" class="hover:text-gray-300">Contact</a> |
|
</div> |
|
</div> |
|
<div class="mt-4 text-xs text-gray-600"> |
|
<p>Disclaimer: This tool analyzes publicly available data for research purposes only. Results should not be considered definitive psychological assessments.</p> |
|
</div> |
|
</footer> |
|
</div> |
|
|
|
<script> |
|
|
|
let sentimentChart; |
|
let allPosts = []; |
|
let analyzedData = {}; |
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
const analyzeBtn = document.getElementById('analyzeBtn'); |
|
const profileUrl = document.getElementById('profileUrl'); |
|
const resultsSection = document.getElementById('resultsSection'); |
|
const loadingState = document.getElementById('loadingState'); |
|
const downloadReport = document.getElementById('downloadReport'); |
|
|
|
analyzeBtn.addEventListener('click', function() { |
|
const input = profileUrl.value.trim(); |
|
|
|
if (!input) { |
|
alert('Please enter a valid Reddit username'); |
|
return; |
|
} |
|
|
|
|
|
let username = input.startsWith('u/') ? input.substring(2) : input; |
|
username = username.split('/')[0].split('?')[0].trim(); |
|
|
|
if (!username) { |
|
alert('Please enter a valid Reddit username'); |
|
return; |
|
} |
|
|
|
|
|
loadingState.classList.remove('hidden'); |
|
resultsSection.classList.add('hidden'); |
|
|
|
|
|
analyzeRedditUser(username); |
|
}); |
|
|
|
downloadReport.addEventListener('click', function() { |
|
if (!analyzedData.username) return; |
|
generatePDFReport(); |
|
}); |
|
|
|
async function analyzeRedditUser(username) { |
|
try { |
|
|
|
document.getElementById('progressBar').style.width = '0%'; |
|
|
|
|
|
updateLoadingMessage(`Fetching profile data for u/${username}...`); |
|
await simulateProgress(10); |
|
|
|
const userData = await fetchRedditUserData(username); |
|
if (!userData) { |
|
throw new Error('User not found or private profile'); |
|
} |
|
|
|
|
|
updateLoadingMessage(`Fetching posts by u/${username}...`); |
|
await simulateProgress(20); |
|
|
|
const posts = await fetchRedditUserPosts(username); |
|
if (!posts || posts.length === 0) { |
|
throw new Error('No public posts found'); |
|
} |
|
|
|
allPosts = posts; |
|
|
|
|
|
updateLoadingMessage(`Analyzing ${posts.length} posts...`); |
|
await simulateProgress(40); |
|
|
|
const analysisResults = await analyzePosts(posts); |
|
|
|
|
|
updateLoadingMessage(`Compiling ideological fingerprint...`); |
|
await simulateProgress(70); |
|
|
|
|
|
document.getElementById('progressBar').style.width = '100%'; |
|
|
|
|
|
displayResults(username, userData, analysisResults); |
|
|
|
|
|
setTimeout(() => { |
|
loadingState.classList.add('hidden'); |
|
resultsSection.classList.remove('hidden'); |
|
|
|
|
|
resultsSection.scrollIntoView({ behavior: 'smooth' }); |
|
}, 500); |
|
|
|
} catch (error) { |
|
console.error('Analysis failed:', error); |
|
loadingState.classList.add('hidden'); |
|
alert(`Analysis failed: ${error.message}`); |
|
} |
|
} |
|
|
|
async function fetchRedditUserData(username) { |
|
|
|
|
|
|
|
try { |
|
const response = await fetch(`https://www.reddit.com/user/${username}/about.json`); |
|
if (!response.ok) throw new Error('User not found'); |
|
|
|
const data = await response.json(); |
|
if (data.error) throw new Error(data.message); |
|
|
|
return data.data; |
|
} catch (error) { |
|
console.error('Error fetching user data:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
async function fetchRedditUserPosts(username) { |
|
|
|
try { |
|
const response = await fetch(`https://www.reddit.com/user/${username}/submitted.json?limit=100`); |
|
if (!response.ok) throw new Error('Failed to fetch posts'); |
|
|
|
const data = await response.json(); |
|
if (data.error) throw new Error(data.message); |
|
|
|
return data.data.children.map(child => child.data); |
|
} catch (error) { |
|
console.error('Error fetching posts:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
async function analyzePosts(posts) { |
|
|
|
|
|
|
|
const results = { |
|
totalPosts: posts.length, |
|
earliestPost: new Date(Math.min(...posts.map(p => p.created_utc * 1000))), |
|
latestPost: new Date(Math.max(...posts.map(p => p.created_utc * 1000))), |
|
sentimentScores: [], |
|
topics: {}, |
|
controversialPosts: [], |
|
timeline: [] |
|
}; |
|
|
|
|
|
posts.forEach(post => { |
|
|
|
const text = post.title + ' ' + (post.selftext || ''); |
|
const sentiment = analyzeSentiment(text); |
|
results.sentimentScores.push({ |
|
date: new Date(post.created_utc * 1000), |
|
score: sentiment.score, |
|
magnitude: sentiment.magnitude |
|
}); |
|
|
|
|
|
if (post.downvotes > 0 || post.controversiality > 0) { |
|
results.controversialPosts.push({ |
|
title: post.title, |
|
text: post.selftext, |
|
score: post.score, |
|
url: `https://reddit.com${post.permalink}`, |
|
date: new Date(post.created_utc * 1000) |
|
}); |
|
} |
|
|
|
|
|
const doc = window.nlp(text); |
|
const nouns = doc.nouns().out('array'); |
|
const adjectives = doc.adjectives().out('array'); |
|
|
|
nouns.forEach(noun => { |
|
results.topics[noun] = (results.topics[noun] || 0) + 1; |
|
}); |
|
|
|
adjectives.forEach(adj => { |
|
results.topics[adj] = (results.topics[adj] || 0) + 1; |
|
}); |
|
}); |
|
|
|
|
|
results.controversialPosts.sort((a, b) => b.score - a.score); |
|
|
|
|
|
const topicEntries = Object.entries(results.topics) |
|
.filter(([word]) => word.length > 3) |
|
.sort((a, b) => b[1] - a[1]) |
|
.slice(0, 20); |
|
|
|
results.topTopics = topicEntries.map(([text, count]) => ({ |
|
text, |
|
weight: count / posts.length |
|
})); |
|
|
|
|
|
const postsByYear = {}; |
|
posts.forEach(post => { |
|
const year = new Date(post.created_utc * 1000).getFullYear(); |
|
if (!postsByYear[year]) postsByYear[year] = []; |
|
postsByYear[year].push(post); |
|
}); |
|
|
|
|
|
Object.entries(postsByYear).forEach(([year, yearPosts]) => { |
|
if (yearPosts.length < 3) return; |
|
|
|
|
|
const yearText = yearPosts.map(p => p.title + ' ' + (p.selftext || '')).join(' '); |
|
const yearSentiment = analyzeSentiment(yearText); |
|
|
|
|
|
const yearDoc = window.nlp(yearText); |
|
const yearNouns = yearDoc.nouns().out('array'); |
|
const yearTopics = {}; |
|
|
|
yearNouns.forEach(noun => { |
|
if (noun.length > 3) { |
|
yearTopics[noun] = (yearTopics[noun] || 0) + 1; |
|
} |
|
}); |
|
|
|
const topYearTopics = Object.entries(yearTopics) |
|
.sort((a, b) => b[1] - a[1]) |
|
.slice(0, 3) |
|
.map(([topic]) => topic); |
|
|
|
|
|
results.timeline.push({ |
|
year: year.toString(), |
|
title: getTimelineTitle(year, yearSentiment.score), |
|
description: getTimelineDescription(year, yearSentiment.score, topYearTopics), |
|
sentiment: yearSentiment.score > 0.2 ? 'positive' : yearSentiment.score < -0.2 ? 'negative' : 'neutral', |
|
tags: topYearTopics |
|
}); |
|
}); |
|
|
|
return results; |
|
} |
|
|
|
function analyzeSentiment(text) { |
|
|
|
|
|
const positiveWords = ['good', 'great', 'awesome', 'happy', 'love', 'best', 'excellent', 'positive']; |
|
const negativeWords = ['bad', 'terrible', 'awful', 'hate', 'worst', 'negative', 'angry', 'sad']; |
|
|
|
const words = text.toLowerCase().split(/\s+/); |
|
let positiveCount = 0; |
|
let negativeCount = 0; |
|
let total = 0; |
|
|
|
words.forEach(word => { |
|
if (positiveWords.includes(word)) { |
|
positiveCount++; |
|
total++; |
|
} else if (negativeWords.includes(word)) { |
|
negativeCount++; |
|
total++; |
|
} |
|
}); |
|
|
|
const score = total > 0 ? (positiveCount - negativeCount) / total : 0; |
|
const magnitude = total > 0 ? (positiveCount + negativeCount) / total : 0; |
|
|
|
return { score, magnitude }; |
|
} |
|
|
|
function getTimelineTitle(year, sentimentScore) { |
|
const descriptors = [ |
|
"Neutral Period", |
|
"Active Year", |
|
"Productive Phase", |
|
"Engagement Spike" |
|
]; |
|
|
|
if (sentimentScore > 0.3) { |
|
return "Positive Outlook"; |
|
} else if (sentimentScore < -0.3) { |
|
return "Negative Phase"; |
|
} |
|
|
|
return descriptors[year % descriptors.length]; |
|
} |
|
|
|
function getTimelineDescription(year, sentimentScore, topics) { |
|
let desc = `In ${year}, the user frequently discussed ${topics.join(', ')}. `; |
|
|
|
if (sentimentScore > 0.3) { |
|
desc += "Their language was predominantly positive, showing optimism in their posts."; |
|
} else if (sentimentScore < -0.3) { |
|
desc += "Their language showed signs of negativity, with critical or pessimistic tones."; |
|
} else { |
|
desc += "Their language was generally neutral, with balanced emotional expression."; |
|
} |
|
|
|
return desc; |
|
} |
|
|
|
function displayResults(username, userData, analysisResults) { |
|
|
|
analyzedData = { |
|
username, |
|
userData, |
|
analysisResults |
|
}; |
|
|
|
|
|
document.getElementById('profileName').textContent = userData.name || username; |
|
document.getElementById('profileHandle').textContent = `u/${username}`; |
|
document.getElementById('avatarInitial').textContent = username.charAt(0).toUpperCase(); |
|
|
|
|
|
if (userData.icon_img) { |
|
document.getElementById('avatarContainer').innerHTML = ` |
|
<img src="${userData.icon_img}" class="w-full h-full rounded-full object-cover" alt="Profile"> |
|
`; |
|
} |
|
|
|
|
|
document.getElementById('postCount').textContent = analysisResults.totalPosts; |
|
|
|
const yearsActive = analysisResults.latestPost.getFullYear() - analysisResults.earliestPost.getFullYear() + 1; |
|
document.getElementById('timeSpan').textContent = `${yearsActive} year${yearsActive !== 1 ? 's' : ''}`; |
|
|
|
|
|
const avgSentiment = analysisResults.sentimentScores.reduce((sum, item) => sum + item.score, 0) / analysisResults.sentimentScores.length; |
|
const ideologyScore = Math.round(50 + (avgSentiment * 50)); |
|
document.getElementById('ideologyScore').textContent = `${ideologyScore}/100`; |
|
|
|
|
|
const variance = analysisResults.sentimentScores.reduce((sum, item) => { |
|
return sum + Math.pow(item.score - avgSentiment, 2); |
|
}, 0) / analysisResults.s |
|
</html> |