Custom Code Experiment •
Keyword Extractor
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyword Extractor</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-color: #3b82f6;
--secondary-color: #1e40af;
--accent-color: #60a5fa;
--background-color: #000000;
--card-bg: #111111;
--text-color: #ffffff;
--border-color: #333333;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--gray-color: #666666;
--border-radius: 4px;
--transition: all 0.2s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.4;
color: var(--text-color);
background-color: var(--background-color);
min-height: 100vh;
padding: 15px;
font-size: 13px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.app-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 8px;
}
.input-section {
background-color: var(--card-bg);
padding: 15px;
border-radius: var(--border-radius);
margin-bottom: 15px;
border: 1px solid var(--border-color);
}
.url-input-container {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
#urlInput {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 13px;
transition: var(--transition);
background-color: #1a1a1a;
color: var(--text-color);
}
#urlInput:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 16px;
border-radius: var(--border-radius);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.btn-primary:hover {
background-color: var(--secondary-color);
}
.options {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.option-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: var(--text-color);
font-size: 12px;
}
.checkbox-label input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
accent-color: var(--primary-color);
}
.option-group label:not(.checkbox-label) {
font-weight: 500;
color: var(--text-color);
font-size: 12px;
}
.option-group input[type="number"] {
width: 55px;
padding: 5px 8px;
border: 1px solid var(--border-color);
border-radius: 3px;
text-align: center;
background-color: #1a1a1a;
color: var(--text-color);
font-size: 12px;
}
.loading {
padding: 30px 15px;
margin-bottom: 15px;
background-color: var(--card-bg);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top: 3px solid var(--primary-color);
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results-section {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 15px;
border: 1px solid var(--border-color);
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.results-header h2 {
color: var(--primary-color);
font-size: 16px;
font-weight: 600;
}
.btn-secondary {
background-color: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
padding: 6px 10px;
border-radius: var(--border-radius);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 4px;
}
.btn-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.results-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 12px;
margin-bottom: 15px;
}
@media (max-width: 1200px) {
.results-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.results-container {
grid-template-columns: 1fr;
}
}
.result-category {
background-color: #1a1a1a;
border-radius: var(--border-radius);
padding: 12px;
border: 1px solid var(--border-color);
}
.result-category h3 {
margin-bottom: 10px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
}
.keywords-list {
min-height: 120px;
max-height: 200px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: flex-start;
padding: 2px;
}
.keyword-tag {
background-color: #222222;
border: 1px solid #333333;
border-radius: 3px;
padding: 4px 8px;
font-size: 11px;
display: inline-flex;
align-items: center;
gap: 4px;
transition: var(--transition);
}
.keyword-tag.high-frequency {
background-color: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.3);
color: var(--accent-color);
}
.keyword-tag.key-phrase {
background-color: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.3);
color: #a78bfa;
}
.keyword-tag.latent {
background-color: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.3);
color: #86efac;
}
.keyword-tag .frequency {
font-size: 10px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 2px;
padding: 1px 4px;
margin-left: 3px;
}
.no-data {
color: var(--gray-color);
font-style: italic;
text-align: left;
width: 100%;
padding: 20px;
font-size: 12px;
}
.stats {
display: flex;
justify-content: space-around;
background-color: #1a1a1a;
padding: 10px;
border-radius: var(--border-radius);
margin-top: 12px;
border: 1px solid var(--border-color);
}
.stat {
text-align: left;
}
.stat-label {
display: block;
font-weight: 500;
color: var(--gray-color);
margin-bottom: 3px;
font-size: 11px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.info-section {
background-color: var(--card-bg);
padding: 12px;
border-radius: var(--border-radius);
margin-top: 15px;
border: 1px solid var(--border-color);
}
.info-section h3 {
margin-bottom: 8px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
}
.info-section ul {
margin-left: 15px;
margin-bottom: 8px;
}
.info-section li {
margin-bottom: 3px;
color: var(--text-color);
font-size: 11px;
line-height: 1.4;
}
.note {
padding: 8px 10px;
background-color: rgba(245, 158, 11, 0.1);
border-left: 3px solid var(--warning-color);
border-radius: 2px;
margin-top: 8px;
color: var(--warning-color);
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 11px;
}
.hidden {
display: none !important;
}
.notification {
position: fixed;
top: 15px;
right: 15px;
padding: 10px 14px;
border-radius: var(--border-radius);
color: white;
font-weight: 500;
z-index: 1000;
font-size: 12px;
animation: slideIn 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
max-width: 350px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.notification.success {
background-color: var(--success-color);
}
.notification.warning {
background-color: var(--warning-color);
color: #000;
}
.notification.error {
background-color: var(--error-color);
}
.notification.info {
background-color: var(--primary-color);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.keywords-list::-webkit-scrollbar {
width: 6px;
}
.keywords-list::-webkit-scrollbar-track {
background: #1a1a1a;
border-radius: 3px;
}
.keywords-list::-webkit-scrollbar-thumb {
background: var(--gray-color);
border-radius: 3px;
}
.keywords-list::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.url-input-container {
flex-direction: column;
}
.btn-primary {
width: 100%;
justify-content: center;
}
.options {
flex-direction: column;
gap: 10px;
}
.option-group {
justify-content: space-between;
}
.stats {
flex-direction: column;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="input-section">
<div class="url-input-container">
<input type="url" id="urlInput"
placeholder="Enter URL (e.g., https://en.wikipedia.org/wiki/Artificial_intelligence)"
value="https://en.wikipedia.org/wiki/Artificial_intelligence">
<button id="extractBtn" class="btn-primary">
<i class="fas fa-magic"></i> Extract Keywords
</button>
</div>
<div class="options">
<div class="option-group">
<label class="checkbox-label">
<input type="checkbox" id="removeStopwords" checked>
<span>Remove stop words</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="stemWords">
<span>Stem words</span>
</label>
</div>
<div class="option-group">
<label for="maxKeywords">Max Keywords:</label>
<input type="number" id="maxKeywords" min="5" max="100" value="20">
<label for="minWordLength">Min Word Length:</label>
<input type="number" id="minWordLength" min="2" max="10" value="3">
</div>
</div>
</div>
<div class="loading hidden" id="loading">
<div class="spinner"></div>
<p>Processing webpage content...</p>
</div>
<div class="results-section" id="resultsSection">
<div class="results-header">
<h2><i class="fas fa-chart-bar"></i> Extracted Keywords</h2>
<button id="copyBtn" class="btn-secondary">
<i class="far fa-copy"></i> Copy Results
</button>
</div>
<div class="results-container">
<div class="result-category">
<h3><i class="fas fa-fire"></i> High-Frequency Keywords</h3>
<div class="keywords-list" id="highFrequencyKeywords">
<p class="no-data">Enter a URL and click "Extract Keywords"</p>
</div>
</div>
<div class="result-category">
<h3><i class="fas fa-comments"></i> Key Phrases</h3>
<div class="keywords-list" id="keyPhrases">
<p class="no-data">Enter a URL and click "Extract Keywords"</p>
</div>
</div>
<div class="result-category">
<h3><i class="fas fa-brain"></i> Latent Keywords</h3>
<div class="keywords-list" id="latentKeywords">
<p class="no-data">Enter a URL and click "Extract Keywords"</p>
</div>
</div>
</div>
<div class="stats">
<div class="stat">
<span class="stat-label">Words Processed:</span>
<span class="stat-value" id="wordsProcessed">0</span>
</div>
<div class="stat">
<span class="stat-label">Unique Words:</span>
<span class="stat-value" id="uniqueWords">0</span>
</div>
<div class="stat">
<span class="stat-label">Extraction Time:</span>
<span class="stat-value" id="extractionTime">0s</span>
</div>
</div>
</div>
</div>
<script>
const CORS_PROXIES = [
'https://api.allorigins.win/raw?url=',
'https://api.codetabs.com/v1/proxy?quest=',
'https://corsproxy.io/?',
(url) => `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`
];
const STOP_WORDS = new Set([
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
'will', 'would', 'shall', 'should', 'may', 'might', 'must', 'can', 'could', 'i', 'you', 'he',
'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her',
'its', 'our', 'their', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were',
'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'shall',
'should', 'may', 'might', 'must', 'can', 'could', 'i', 'you', 'he', 'she', 'it', 'we', 'they',
'what', 'which', 'who', 'whom', 'whose', 'where', 'when', 'why', 'how', 'all', 'any', 'both',
'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own',
'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'
]);
const proxyHealth = new Map();
let lastRequestTime = 0;
const MIN_REQUEST_INTERVAL = 2000;
function porterStemmer(word) {
if (word.length < 3) return word.toLowerCase();
word = word.toLowerCase();
if (word.endsWith('sses')) {
word = word.slice(0, -2);
} else if (word.endsWith('ies')) {
word = word.slice(0, -2);
} else if (word.endsWith('ss')) {
} else if (word.endsWith('s')) {
word = word.slice(0, -1);
}
if (word.match(/(eed|eedly)$/)) {
} else if (word.match(/[aeiou].*(ed|edly|ing|ingly)$/)) {
word = word.replace(/(ed|edly|ing|ingly)$/, '');
if (word.match(/(at|bl|iz)$/)) {
word += 'e';
} else if (word.match(/([^aeiouylsz])\1$/)) {
word = word.slice(0, -1);
} else if (word.match(/^[^aeiou][aeiouy][^aeiouwxy]$/)) {
word += 'e';
}
}
return word;
}
function extractTextFromHTML(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const scripts = doc.querySelectorAll('script, style, noscript, iframe, svg, nav, header, footer');
scripts.forEach(el => el.remove());
let content = '';
const wikipediaSelectors = [
'#mw-content-text',
'.mw-parser-output',
'#bodyContent',
'.mw-body'
];
for (const selector of wikipediaSelectors) {
const element = doc.querySelector(selector);
if (element && element.textContent && element.textContent.trim().length > 1000) {
content = element.textContent;
break;
}
}
if (!content || content.length < 1000) {
const generalSelectors = [
'article',
'main',
'.content',
'.post-content',
'.entry-content',
'.article-content',
'#content',
'#main-content',
'section'
];
for (const selector of generalSelectors) {
const element = doc.querySelector(selector);
if (element && element.textContent && element.textContent.trim().length > 1000) {
content = element.textContent;
break;
}
}
}
if (!content || content.length < 1000) {
const allElements = doc.querySelectorAll('p, div, section, article');
let largestText = '';
let largestLength = 0;
allElements.forEach(element => {
const text = element.textContent || '';
const textLength = text.trim().length;
if (textLength > largestLength && textLength > 500) {
largestLength = textLength;
largestText = text;
}
});
if (largestText && largestLength > 1000) {
content = largestText;
}
}
if (!content || content.length < 1000) {
content = doc.body.textContent || '';
}
if (content) {
content = content.replace(/\s+/g, ' ').trim();
content = content.replace(/\[\d+\]/g, '');
content = content.replace(/\([^)]*\)/g, ' ');
content = content.replace(/[^\w\s.,!?-]/g, ' ');
content = content.replace(/\s+/g, ' ').trim();
}
return content || '';
}
function tokenizeText(text, options = {}) {
const { removeStopwords = true, stemWords = false, minWordLength = 3 } = options;
if (!text || text.trim().length === 0) {
return [];
}
let tokens = text.toLowerCase()
.replace(/[^\w\s-]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.split(' ');
tokens = tokens.filter(token => {
if (token.length < minWordLength) return false;
if (/^\d+$/.test(token)) return false;
return true;
});
if (removeStopwords) {
tokens = tokens.filter(token => !STOP_WORDS.has(token));
}
if (stemWords) {
tokens = tokens.map(token => porterStemmer(token));
}
return tokens;
}
function extractNGrams(tokens, n, minWordLength) {
const ngrams = [];
for (let i = 0; i <= tokens.length - n; i++) {
const slice = tokens.slice(i, i + n);
const isValid = slice.every(word => word.length >= minWordLength);
if (isValid) {
ngrams.push(slice.join(' '));
}
}
return ngrams;
}
function calculateKeywordScores(words) {
const frequencies = {};
words.forEach(word => {
frequencies[word] = (frequencies[word] || 0) + 1;
});
return { frequencies };
}
function findLatentKeywords(keywords, allWords, windowSize = 5, minWordLength) {
const coOccurrence = {};
for (let i = 0; i < allWords.length; i++) {
const windowStart = Math.max(0, i - windowSize);
const windowEnd = Math.min(allWords.length, i + windowSize);
const currentWord = allWords[i];
if (keywords.includes(currentWord)) {
for (let j = windowStart; j < windowEnd; j++) {
if (i === j) continue;
const neighborWord = allWords[j];
if (!keywords.includes(neighborWord) && neighborWord.length >= minWordLength) {
coOccurrence[neighborWord] = (coOccurrence[neighborWord] || 0) + 1;
}
}
}
}
return Object.entries(coOccurrence)
.sort((a, b) => b[1] - a[1])
.slice(0, 15)
.map(([word, freq]) => ({ word, freq }));
}
async function fetchWithProxy(url) {
for (let i = 0; i < CORS_PROXIES.length; i++) {
const proxy = CORS_PROXIES[i];
try {
let proxyUrl;
if (typeof proxy === 'function') {
proxyUrl = proxy(url);
} else {
proxyUrl = proxy + encodeURIComponent(url);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
const response = await fetch(proxyUrl, {
signal: controller.signal,
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let content;
if (proxyUrl.includes('api.allorigins.win/get')) {
const data = await response.json();
content = data.contents;
} else {
content = await response.text();
}
if (!content || content.length < 100) {
throw new Error('Empty response');
}
return content;
} catch (error) {
continue;
}
}
throw new Error('All proxies failed');
}
const urlInput = document.getElementById('urlInput');
const extractBtn = document.getElementById('extractBtn');
const loadingElement = document.getElementById('loading');
const resultsSection = document.getElementById('resultsSection');
const highFrequencyKeywordsEl = document.getElementById('highFrequencyKeywords');
const keyPhrasesEl = document.getElementById('keyPhrases');
const latentKeywordsEl = document.getElementById('latentKeywords');
const wordsProcessedEl = document.getElementById('wordsProcessed');
const uniqueWordsEl = document.getElementById('uniqueWords');
const extractionTimeEl = document.getElementById('extractionTime');
const copyBtn = document.getElementById('copyBtn');
const removeStopwordsCheckbox = document.getElementById('removeStopwords');
const stemWordsCheckbox = document.getElementById('stemWords');
const maxKeywordsInput = document.getElementById('maxKeywords');
const minWordLengthInput = document.getElementById('minWordLength');
function createKeywordTag(text, frequency, type) {
const tag = document.createElement('div');
tag.className = `keyword-tag ${type}`;
const textSpan = document.createElement('span');
textSpan.className = 'keyword-text';
textSpan.textContent = text;
const freqSpan = document.createElement('span');
freqSpan.className = 'frequency';
freqSpan.textContent = frequency;
tag.appendChild(textSpan);
tag.appendChild(freqSpan);
return tag;
}
function displayKeywords(keywords, element, type) {
element.innerHTML = '';
if (keywords.length === 0) {
const noData = document.createElement('p');
noData.className = 'no-data';
noData.textContent = 'No keywords found';
element.appendChild(noData);
return;
}
keywords.forEach(item => {
const tag = createKeywordTag(item.word, item.freq, type);
element.appendChild(tag);
});
}
function showNotification(message, type = 'info', duration = 4000) {
const existing = document.querySelectorAll('.notification');
existing.forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icons = {
success: 'fas fa-check-circle',
warning: 'fas fa-exclamation-triangle',
error: 'fas fa-exclamation-circle',
info: 'fas fa-info-circle'
};
notification.innerHTML = `
<i class="${icons[type] || icons.info}"></i>
<span>${message}</span>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, duration);
notification.addEventListener('click', () => notification.remove());
}
function validateAndNormalizeUrl(url) {
let normalizedUrl = url.trim();
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
normalizedUrl = 'https://' + normalizedUrl;
}
normalizedUrl = normalizedUrl.replace(/\/+$/, '');
return normalizedUrl;
}
async function extractKeywords() {
const now = Date.now();
if (now - lastRequestTime < MIN_REQUEST_INTERVAL) {
showNotification(`Wait ${Math.ceil((MIN_REQUEST_INTERVAL - (now - lastRequestTime)) / 1000)}s`, 'warning');
return;
}
lastRequestTime = now;
const url = urlInput.value.trim();
if (!url) {
showNotification('Enter URL', 'warning');
return;
}
try {
urlInput.value = validateAndNormalizeUrl(url);
new URL(urlInput.value);
} catch (error) {
showNotification('Invalid URL', 'warning');
return;
}
loadingElement.classList.remove('hidden');
resultsSection.classList.add('hidden');
highFrequencyKeywordsEl.innerHTML = '<p class="no-data">Processing...</p>';
keyPhrasesEl.innerHTML = '<p class="no-data">Processing...</p>';
latentKeywordsEl.innerHTML = '<p class="no-data">Processing...</p>';
const startTime = Date.now();
try {
loadingElement.querySelector('p').textContent = 'Fetching webpage...';
const html = await fetchWithProxy(urlInput.value);
loadingElement.querySelector('p').textContent = 'Extracting text...';
const text = extractTextFromHTML(html);
if (!text || text.trim().length < 500) {
throw new Error(`Not enough content (${text ? text.length : 0} chars)`);
}
loadingElement.querySelector('p').textContent = 'Processing keywords...';
const options = {
removeStopwords: removeStopwordsCheckbox.checked,
stemWords: stemWordsCheckbox.checked,
minWordLength: parseInt(minWordLengthInput.value)
};
const tokens = tokenizeText(text, options);
if (tokens.length < 10) {
throw new Error(`Insufficient content (${tokens.length} tokens)`);
}
const uniqueWords = new Set(tokens).size;
const maxKeywords = parseInt(maxKeywordsInput.value);
const minWordLength = parseInt(minWordLengthInput.value);
const { frequencies: wordFrequencies } = calculateKeywordScores(tokens);
const highFrequencyKeywords = Object.entries(wordFrequencies)
.sort((a, b) => b[1] - a[1])
.slice(0, maxKeywords)
.map(([word, freq]) => ({ word, freq }));
const bigrams = extractNGrams(tokens, 2, minWordLength);
const trigrams = extractNGrams(tokens, 3, minWordLength);
const allPhrases = [...bigrams, ...trigrams];
const { frequencies: phraseFrequencies } = calculateKeywordScores(allPhrases);
const keyPhrases = Object.entries(phraseFrequencies)
.sort((a, b) => b[1] - a[1])
.slice(0, Math.floor(maxKeywords / 2))
.map(([phrase, freq]) => ({ word: phrase, freq }));
const topKeywords = highFrequencyKeywords.slice(0, 20).map(k => k.word);
const latentKeywords = findLatentKeywords(topKeywords, tokens, 5, minWordLength);
const endTime = Date.now();
const extractionTime = ((endTime - startTime) / 1000).toFixed(2);
wordsProcessedEl.textContent = tokens.length.toLocaleString();
uniqueWordsEl.textContent = uniqueWords.toLocaleString();
extractionTimeEl.textContent = `${extractionTime}s`;
displayKeywords(highFrequencyKeywords, highFrequencyKeywordsEl, 'high-frequency');
displayKeywords(keyPhrases, keyPhrasesEl, 'key-phrase');
displayKeywords(latentKeywords, latentKeywordsEl, 'latent');
window.lastResults = {
highFrequencyKeywords,
keyPhrases,
latentKeywords,
url: urlInput.value,
timestamp: new Date().toISOString()
};
resultsSection.classList.remove('hidden');
showNotification(`Extracted ${tokens.length.toLocaleString()} words`, 'success');
} catch (error) {
console.error('Extraction error:', error);
let errorMessage;
if (error.message.includes('All proxies failed')) {
errorMessage = 'Unable to fetch webpage';
} else if (error.message.includes('Not enough content')) {
errorMessage = 'Not enough text content';
} else if (error.message.includes('Insufficient content')) {
errorMessage = 'Insufficient content for analysis';
} else {
errorMessage = `Error: ${error.message}`;
}
showNotification(errorMessage, 'error');
highFrequencyKeywordsEl.innerHTML = '<p class="no-data">Extraction failed</p>';
keyPhrasesEl.innerHTML = '<p class="no-data">Extraction failed</p>';
latentKeywordsEl.innerHTML = '<p class="no-data">Extraction failed</p>';
wordsProcessedEl.textContent = '0';
uniqueWordsEl.textContent = '0';
extractionTimeEl.textContent = '0s';
resultsSection.classList.remove('hidden');
} finally {
loadingElement.classList.add('hidden');
}
}
function copyResultsToClipboard() {
if (!window.lastResults) {
showNotification('No results', 'warning');
return;
}
const results = window.lastResults;
let text = `Keyword Extraction Results\n`;
text += `URL: ${results.url}\n`;
text += `Extracted: ${new Date(results.timestamp).toLocaleString()}\n\n`;
text += `HIGH-FREQUENCY KEYWORDS:\n`;
results.highFrequencyKeywords.forEach((item, i) => {
text += `${i + 1}. ${item.word} (${item.freq})\n`;
});
text += `\nKEY PHRASES:\n`;
results.keyPhrases.forEach((item, i) => {
text += `${i + 1}. ${item.word} (${item.freq})\n`;
});
text += `\nLATENT SEMANTIC KEYWORDS:\n`;
results.latentKeywords.forEach((item, i) => {
text += `${i + 1}. ${item.word} (${item.freq})\n`;
});
navigator.clipboard.writeText(text).then(() => {
showNotification('Copied to clipboard', 'success', 2000);
copyBtn.innerHTML = '<i class="fas fa-check"></i> Copied';
setTimeout(() => {
copyBtn.innerHTML = '<i class="far fa-copy"></i> Copy Results';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
showNotification('Copy failed', 'error');
});
}
extractBtn.addEventListener('click', extractKeywords);
urlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') extractKeywords();
});
copyBtn.addEventListener('click', copyResultsToClipboard);
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
showNotification('Ready to extract keywords', 'info', 3000);
}, 500);
});
</script>
</body>
</html>