1034 lines
40 KiB
JavaScript
1034 lines
40 KiB
JavaScript
// Twitter API base URL
|
|
// const API_BASE = 'http://localhost:3343/api' //
|
|
const API_BASE = 'https://tweets.nunosempere.com/api';
|
|
|
|
// DOM elements
|
|
const healthCheckBtn = document.getElementById('health-check');
|
|
const healthResultDiv = document.getElementById('health-result');
|
|
|
|
const addUsernameInput = document.getElementById('add-username');
|
|
const addAccountBtn = document.getElementById('add-account');
|
|
const bulkUsernamesInput = document.getElementById('bulk-usernames');
|
|
const bulkAddAccountsBtn = document.getElementById('bulk-add-accounts');
|
|
const bulkResultDiv = document.getElementById('bulk-result');
|
|
const showAccountsBtn = document.getElementById('show-accounts');
|
|
const hideAccountsBtn = document.getElementById('hide-accounts');
|
|
const accountResultDiv = document.getElementById('account-result');
|
|
const monitoredAccountsResultDiv = document.getElementById('monitored-accounts-result');
|
|
|
|
const showListsBtn = document.getElementById('show-lists');
|
|
const hideListsBtn = document.getElementById('hide-lists');
|
|
const listsResultDiv = document.getElementById('lists-result');
|
|
const newListNameInput = document.getElementById('new-list-name');
|
|
const listUsernamesInput = document.getElementById('list-usernames');
|
|
const createListBtn = document.getElementById('create-list');
|
|
const createListResultDiv = document.getElementById('create-list-result');
|
|
|
|
const editListNameInput = document.getElementById('edit-list-name');
|
|
const editListUsernamesInput = document.getElementById('edit-list-usernames');
|
|
const editListPasswordInput = document.getElementById('edit-list-password');
|
|
const editListBtn = document.getElementById('edit-list');
|
|
const editListResultDiv = document.getElementById('edit-list-result');
|
|
|
|
const tweetsLimitInput = document.getElementById('tweets-limit');
|
|
const tweetsListInput = document.getElementById('tweets-list');
|
|
const getAllTweetsBtn = document.getElementById('get-all-tweets');
|
|
const userTweetsUsernameInput = document.getElementById('user-tweets-username');
|
|
const userTweetsLimitInput = document.getElementById('user-tweets-limit');
|
|
const getUserTweetsBtn = document.getElementById('get-user-tweets');
|
|
const tweetsResultDiv = document.getElementById('tweets-result');
|
|
const userTweetsResultDiv = document.getElementById('user-tweets-result');
|
|
const hideAllTweetsBtn = document.getElementById('hide-all-tweets');
|
|
const hideUserTweetsBtn = document.getElementById('hide-user-tweets');
|
|
|
|
const filterQuestionInput = document.getElementById('filter-question');
|
|
const summarizationQuestionInput = document.getElementById('summarization-question');
|
|
const filterListInput = document.getElementById('filter-list');
|
|
const filterUsersInput = document.getElementById('filter-users');
|
|
const filterTweetsBtn = document.getElementById('filter-tweets');
|
|
const hideFilterResultsBtn = document.getElementById('hide-filter-results');
|
|
const filterResultDiv = document.getElementById('filter-result');
|
|
|
|
// Helper functions
|
|
function showError(container, message) {
|
|
container.innerHTML = `<div class="error">${message}</div>`;
|
|
container.classList.add('show');
|
|
}
|
|
|
|
function showSuccess(container, message) {
|
|
container.innerHTML = `<div class="success">${message}</div>`;
|
|
container.classList.add('show');
|
|
}
|
|
|
|
function showResults(container, html) {
|
|
container.innerHTML = html;
|
|
container.classList.add('show');
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
return new Date(dateString).toLocaleString();
|
|
}
|
|
|
|
function truncateText(text, maxLength = 100) {
|
|
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
|
}
|
|
|
|
// Parse markdown to HTML using marked library
|
|
function parseMarkdown(text) {
|
|
if (!text || typeof marked === 'undefined') return text;
|
|
return marked.parse(text);
|
|
}
|
|
|
|
// API request helper
|
|
async function apiRequest(endpoint, options = {}) {
|
|
try {
|
|
const controller = new AbortController();
|
|
// const timeoutId = setTimeout(() => controller.abort(), options.timeout || 30000);
|
|
|
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
},
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
|
|
// clearTimeout(timeoutId);
|
|
|
|
console.log(response)
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || `HTTP error! status: ${response.status}`);
|
|
}
|
|
console.log(data)
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Health Check Handler
|
|
healthCheckBtn.addEventListener('click', async () => {
|
|
healthCheckBtn.disabled = true;
|
|
healthCheckBtn.textContent = 'Checking...';
|
|
|
|
try {
|
|
const result = await apiRequest('/health');
|
|
console.log(result)
|
|
showSuccess(healthResultDiv, `Server is healthy; status: ${result.status}`);
|
|
} catch (error) {
|
|
showError(healthResultDiv, `Health check failed: ${error.message}`);
|
|
} finally {
|
|
healthCheckBtn.disabled = false;
|
|
healthCheckBtn.textContent = 'Check Server Health';
|
|
}
|
|
});
|
|
|
|
// Add Account Handler
|
|
addAccountBtn.addEventListener('click', async () => {
|
|
const username = addUsernameInput.value.trim();
|
|
|
|
if (!username) {
|
|
showError(accountResultDiv, 'Please enter a username.');
|
|
return;
|
|
}
|
|
|
|
addAccountBtn.disabled = true;
|
|
addAccountBtn.textContent = 'Adding...';
|
|
|
|
try {
|
|
const body = { username };
|
|
|
|
const result = await apiRequest('/accounts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
showSuccess(accountResultDiv, `${result.message}`);
|
|
addUsernameInput.value = '';
|
|
} catch (error) {
|
|
showError(accountResultDiv, `Failed to add account: ${error.message}`);
|
|
} finally {
|
|
addAccountBtn.disabled = false;
|
|
addAccountBtn.textContent = 'Add Account';
|
|
}
|
|
});
|
|
|
|
// Bulk Add Accounts Handler
|
|
bulkAddAccountsBtn.addEventListener('click', async () => {
|
|
const usernamesText = bulkUsernamesInput.value.trim();
|
|
|
|
if (!usernamesText) {
|
|
showError(bulkResultDiv, 'Please enter at least one username.');
|
|
return;
|
|
}
|
|
|
|
const usernames = usernamesText.split('\n')
|
|
.map(u => u.trim())
|
|
.filter(u => u && !u.startsWith('@')); // Remove empty lines and @ symbols
|
|
|
|
if (usernames.length === 0) {
|
|
showError(bulkResultDiv, 'Please enter valid usernames.');
|
|
return;
|
|
}
|
|
|
|
bulkAddAccountsBtn.disabled = true;
|
|
bulkAddAccountsBtn.textContent = `Adding ${usernames.length} accounts...`;
|
|
|
|
const results = {
|
|
successful: [],
|
|
failed: []
|
|
};
|
|
|
|
// Show initial progress
|
|
showResults(bulkResultDiv, `<div>Processing ${usernames.length} accounts...</div>`);
|
|
|
|
for (let i = 0; i < usernames.length; i++) {
|
|
const username = usernames[i];
|
|
|
|
try {
|
|
const result = await apiRequest('/accounts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username })
|
|
});
|
|
|
|
results.successful.push({ username, message: result.message });
|
|
} catch (error) {
|
|
results.failed.push({ username, error: error.message });
|
|
}
|
|
|
|
// Update progress
|
|
const progress = Math.round(((i + 1) / usernames.length) * 100);
|
|
showResults(bulkResultDiv, `<div>Processing ${usernames.length} accounts... ${progress}% complete (${i + 1}/${usernames.length})</div>`);
|
|
}
|
|
|
|
// Show final results
|
|
let html = `<h3>Bulk Import Results</h3>`;
|
|
html += `<p><strong>Total:</strong> ${usernames.length} accounts processed</p>`;
|
|
html += `<p><strong>Successful:</strong> ${results.successful.length}</p>`;
|
|
html += `<p><strong>Failed:</strong> ${results.failed.length}</p>`;
|
|
|
|
if (results.successful.length > 0) {
|
|
html += '<h4 style="color: #2e7d32; margin-top: 15px;">Successfully Added:</h4>';
|
|
html += '<ul class="results-list">';
|
|
results.successful.forEach(item => {
|
|
html += `<li><span class="method-name">@${item.username}</span></li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
if (results.failed.length > 0) {
|
|
html += '<h4 style="color: #d32f2f; margin-top: 15px;">Failed to Add:</h4>';
|
|
html += '<ul class="results-list">';
|
|
results.failed.forEach(item => {
|
|
html += `<li><span class="method-name">@${item.username}</span> - ${item.error}</li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
showResults(bulkResultDiv, html);
|
|
|
|
// Clear input if all successful
|
|
if (results.failed.length === 0) {
|
|
bulkUsernamesInput.value = '';
|
|
}
|
|
|
|
bulkAddAccountsBtn.disabled = false;
|
|
bulkAddAccountsBtn.textContent = 'Add All Accounts';
|
|
});
|
|
|
|
// Show Lists Handler
|
|
showListsBtn.addEventListener('click', async () => {
|
|
showListsBtn.disabled = true;
|
|
showListsBtn.textContent = 'Loading...';
|
|
|
|
try {
|
|
const result = await apiRequest('/lists');
|
|
|
|
if (result.data && result.data.length > 0) {
|
|
let html = '<h3>All Lists</h3>';
|
|
html += '<div class="list-container">';
|
|
html += '<div class="list-header"><div>List Name</div><div>Accounts</div><div>Actions</div></div>';
|
|
result.data.forEach((list, index) => {
|
|
const accountCount = list.count || 0;
|
|
const listId = `list-${index}`;
|
|
html += `
|
|
<div class="list-item">
|
|
<div class="list-row">
|
|
<div><span class="method-name">${list.name}</span></div>
|
|
<div><span class="method-value">${accountCount}</span></div>
|
|
<div>
|
|
<button class="secondary-btn toggle-list-btn" data-listid="${listId}">Show Accounts</button>
|
|
<button class="secondary-btn copy-list-btn" data-listname="${list.name}" data-usernames="${btoa(JSON.stringify(list.usernames || []))}" style="margin-left: 5px;">Copy List</button>
|
|
</div>
|
|
</div>
|
|
<div id="${listId}" class="list-details">
|
|
<strong>Accounts:</strong>
|
|
<div class="account-chips-container">
|
|
${list.usernames && list.usernames.length > 0 ?
|
|
list.usernames.map(username => `<span class="account-chip">@${username}</span>`).join('') :
|
|
'<em>No accounts in this list</em>'
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
showResults(listsResultDiv, html);
|
|
showListsBtn.style.display = 'none';
|
|
hideListsBtn.style.display = 'inline-block';
|
|
} else {
|
|
showResults(listsResultDiv, '<h3>All Lists</h3><p>No lists found in database.</p>');
|
|
showListsBtn.style.display = 'none';
|
|
hideListsBtn.style.display = 'inline-block';
|
|
}
|
|
} catch (error) {
|
|
showError(listsResultDiv, `Failed to get lists: ${error.message}`);
|
|
} finally {
|
|
showListsBtn.disabled = false;
|
|
showListsBtn.textContent = 'Show All Lists';
|
|
}
|
|
});
|
|
|
|
// Hide Lists Handler
|
|
hideListsBtn.addEventListener('click', () => {
|
|
listsResultDiv.classList.remove('show');
|
|
listsResultDiv.innerHTML = '';
|
|
showListsBtn.style.display = 'inline-block';
|
|
hideListsBtn.style.display = 'none';
|
|
});
|
|
|
|
// Create List Handler
|
|
createListBtn.addEventListener('click', async () => {
|
|
const listName = newListNameInput.value.trim();
|
|
const usernamesText = listUsernamesInput.value.trim();
|
|
|
|
if (!listName) {
|
|
showError(createListResultDiv, 'Please enter a list name.');
|
|
return;
|
|
}
|
|
|
|
if (!usernamesText) {
|
|
showError(createListResultDiv, 'Please enter at least one username.');
|
|
return;
|
|
}
|
|
|
|
const usernames = usernamesText.split('\n')
|
|
.map(u => u.trim())
|
|
.filter(u => u && !u.startsWith('@')); // Remove empty lines and @ symbols
|
|
|
|
if (usernames.length === 0) {
|
|
showError(createListResultDiv, 'Please enter valid usernames.');
|
|
return;
|
|
}
|
|
|
|
createListBtn.disabled = true;
|
|
createListBtn.textContent = 'Creating List...';
|
|
|
|
try {
|
|
const body = {
|
|
name: listName,
|
|
usernames: usernames
|
|
};
|
|
|
|
const result = await apiRequest('/lists', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
showSuccess(createListResultDiv, `List "${listName}" created successfully with ${usernames.length} accounts.`);
|
|
newListNameInput.value = '';
|
|
listUsernamesInput.value = '';
|
|
} catch (error) {
|
|
showError(createListResultDiv, `Failed to create list: ${error.message}`);
|
|
} finally {
|
|
createListBtn.disabled = false;
|
|
createListBtn.textContent = 'Create List';
|
|
}
|
|
});
|
|
|
|
// Event delegation for list account toggles and copy functionality
|
|
listsResultDiv.addEventListener('click', (event) => {
|
|
if (event.target.classList.contains('toggle-list-btn')) {
|
|
const listId = event.target.dataset.listid;
|
|
const accountsDiv = document.getElementById(listId);
|
|
|
|
if (accountsDiv.classList.contains('show')) {
|
|
accountsDiv.classList.remove('show');
|
|
event.target.textContent = 'Show Accounts';
|
|
} else {
|
|
accountsDiv.classList.add('show');
|
|
event.target.textContent = 'Hide Accounts';
|
|
}
|
|
} else if (event.target.classList.contains('copy-list-btn')) {
|
|
const listName = event.target.dataset.listname;
|
|
const usernames = JSON.parse(atob(event.target.dataset.usernames || btoa('[]')));
|
|
|
|
if (usernames.length === 0) {
|
|
showError(listsResultDiv, `List "${listName}" has no accounts to copy.`);
|
|
return;
|
|
}
|
|
|
|
const textToCopy = usernames.join('\n');
|
|
|
|
// Try to copy to clipboard
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(textToCopy)
|
|
.then(() => {
|
|
// Temporarily change button text to show success
|
|
const originalText = event.target.textContent;
|
|
event.target.textContent = 'Copied!';
|
|
event.target.style.backgroundColor = '#4caf50';
|
|
setTimeout(() => {
|
|
event.target.textContent = originalText;
|
|
event.target.style.backgroundColor = '';
|
|
}, 2000);
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to copy to clipboard:', err);
|
|
fallbackCopy(textToCopy, event.target, listName);
|
|
});
|
|
} else {
|
|
// Fallback for older browsers or non-secure contexts
|
|
fallbackCopy(textToCopy, event.target, listName);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Edit List Handler
|
|
editListBtn.addEventListener('click', async () => {
|
|
const listName = editListNameInput.value.trim();
|
|
const usernamesText = editListUsernamesInput.value.trim();
|
|
const password = editListPasswordInput.value.trim();
|
|
|
|
if (!listName) {
|
|
showError(editListResultDiv, 'Please enter the name of the list to edit.');
|
|
return;
|
|
}
|
|
|
|
if (!usernamesText) {
|
|
showError(editListResultDiv, 'Please enter at least one username.');
|
|
return;
|
|
}
|
|
|
|
if (!password) {
|
|
showError(editListResultDiv, 'Password is required to edit lists.');
|
|
return;
|
|
}
|
|
|
|
const usernames = usernamesText.split('\n')
|
|
.map(u => u.trim())
|
|
.filter(u => u && !u.startsWith('@')); // Remove empty lines and @ symbols
|
|
|
|
if (usernames.length === 0) {
|
|
showError(editListResultDiv, 'Please enter valid usernames.');
|
|
return;
|
|
}
|
|
|
|
editListBtn.disabled = true;
|
|
editListBtn.textContent = 'Editing List...';
|
|
|
|
try {
|
|
const body = {
|
|
usernames: usernames,
|
|
password: password
|
|
};
|
|
|
|
const result = await apiRequest(`/lists/${encodeURIComponent(listName)}/edit`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
showSuccess(editListResultDiv, `List "${listName}" updated successfully with ${usernames.length} accounts.`);
|
|
editListNameInput.value = '';
|
|
editListUsernamesInput.value = '';
|
|
editListPasswordInput.value = '';
|
|
} catch (error) {
|
|
if (error.message.includes('401')) {
|
|
showError(editListResultDiv, 'Incorrect password. Please try again.');
|
|
} else if (error.message.includes('404')) {
|
|
showError(editListResultDiv, `List "${listName}" not found. Please check the list name.`);
|
|
} else {
|
|
showError(editListResultDiv, `Failed to edit list: ${error.message}`);
|
|
}
|
|
} finally {
|
|
editListBtn.disabled = false;
|
|
editListBtn.textContent = 'Edit List';
|
|
}
|
|
});
|
|
|
|
// Fallback copy function for older browsers
|
|
function fallbackCopy(text, button, listName) {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
const successful = document.execCommand('copy');
|
|
if (successful) {
|
|
// Temporarily change button text to show success
|
|
const originalText = button.textContent;
|
|
button.textContent = 'Copied!';
|
|
button.style.backgroundColor = '#4caf50';
|
|
setTimeout(() => {
|
|
button.textContent = originalText;
|
|
button.style.backgroundColor = '';
|
|
}, 2000);
|
|
} else {
|
|
showError(listsResultDiv, `Failed to copy list "${listName}". Please copy manually: ${text}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
showError(listsResultDiv, `Copy not supported in this browser. List "${listName}" usernames: ${text}`);
|
|
} finally {
|
|
document.body.removeChild(textArea);
|
|
}
|
|
}
|
|
|
|
// Show Monitored Accounts Handler
|
|
showAccountsBtn.addEventListener('click', async () => {
|
|
showAccountsBtn.disabled = true;
|
|
showAccountsBtn.textContent = 'Loading...';
|
|
|
|
try {
|
|
const result = await apiRequest('/accounts');
|
|
|
|
if (result.data && result.data.length > 0) {
|
|
let html = '<h3>Monitored Accounts</h3><ul class="results-list">';
|
|
result.data.forEach(account => {
|
|
html += `
|
|
<li>
|
|
<span class="method-name">@${account.username}</span>
|
|
</li>
|
|
`;
|
|
});
|
|
html += '</ul>';
|
|
showResults(monitoredAccountsResultDiv, html);
|
|
showAccountsBtn.style.display = 'none';
|
|
hideAccountsBtn.style.display = 'inline-block';
|
|
} else {
|
|
showResults(monitoredAccountsResultDiv, '<h3>Monitored Accounts</h3><p>No accounts found in database.</p>');
|
|
showAccountsBtn.style.display = 'none';
|
|
hideAccountsBtn.style.display = 'inline-block';
|
|
}
|
|
} catch (error) {
|
|
showError(monitoredAccountsResultDiv, `Failed to get accounts: ${error.message}`);
|
|
} finally {
|
|
showAccountsBtn.disabled = false;
|
|
showAccountsBtn.textContent = 'Show Monitored Accounts';
|
|
}
|
|
});
|
|
|
|
// Hide Accounts Handler
|
|
hideAccountsBtn.addEventListener('click', () => {
|
|
monitoredAccountsResultDiv.classList.remove('show');
|
|
monitoredAccountsResultDiv.innerHTML = '';
|
|
showAccountsBtn.style.display = 'inline-block';
|
|
hideAccountsBtn.style.display = 'none';
|
|
});
|
|
|
|
// Get All Tweets Handler
|
|
getAllTweetsBtn.addEventListener('click', async () => {
|
|
const limit = parseInt(tweetsLimitInput.value) || 100;
|
|
const list = tweetsListInput.value.trim();
|
|
|
|
getAllTweetsBtn.disabled = true;
|
|
getAllTweetsBtn.textContent = 'Loading...';
|
|
|
|
try {
|
|
let endpoint = `/tweets?limit=${limit}`;
|
|
if (list) endpoint += `&list=${encodeURIComponent(list)}`;
|
|
|
|
const result = await apiRequest(endpoint);
|
|
console.log(result)
|
|
|
|
if (result.data.tweets && result.data.tweets.length > 0) {
|
|
let html = `<h3>Tweets (${result.data.tweets.length})</h3>`;
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px;">';
|
|
|
|
result.data.tweets.forEach(tweet => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 10px 0; margin-bottom: 10px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${tweet.username}</div>
|
|
<div style="margin: 5px 0; line-height: 1.4;">${tweet.text}</div>
|
|
<div style="font-size: 0.9em; color: #666;"><a href="https://twitter.com/i/web/status/${tweet.tweet_id}" target="_blank" style="color: #1da1f2; text-decoration: none;">${formatDate(tweet.created_at)}</a></div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
showResults(tweetsResultDiv, html);
|
|
hideAllTweetsBtn.style.display = 'inline-block';
|
|
hideAllTweetsBtn.textContent = 'Hide Results';
|
|
} else {
|
|
showSuccess(tweetsResultDiv, 'No tweets found.');
|
|
}
|
|
} catch (error) {
|
|
showError(tweetsResultDiv, `Failed to get tweets: ${error.message}`);
|
|
} finally {
|
|
getAllTweetsBtn.disabled = false;
|
|
getAllTweetsBtn.textContent = 'Get All Tweets';
|
|
}
|
|
});
|
|
|
|
// Get User Tweets Handler
|
|
getUserTweetsBtn.addEventListener('click', async () => {
|
|
const username = userTweetsUsernameInput.value.trim();
|
|
const limit = parseInt(userTweetsLimitInput.value) || 50;
|
|
|
|
if (!username) {
|
|
showError(userTweetsResultDiv, 'Please enter a username.');
|
|
return;
|
|
}
|
|
|
|
getUserTweetsBtn.disabled = true;
|
|
getUserTweetsBtn.textContent = 'Loading...';
|
|
|
|
try {
|
|
const result = await apiRequest(`/tweets/${username}?limit=${limit}`);
|
|
|
|
if (result.data.tweets && result.data.tweets.length > 0) {
|
|
let html = `<h3>Tweets from @${username} (${result.data.tweets.length})</h3>`;
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px;">';
|
|
|
|
result.data.tweets.forEach(tweet => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 10px 0; margin-bottom: 10px;">
|
|
<div style="margin: 5px 0; line-height: 1.4;">${tweet.text}</div>
|
|
<div style="font-size: 0.9em; color: #666;"><a href="https://twitter.com/i/web/status/${tweet.tweet_id}" target="_blank" style="color: #1da1f2; text-decoration: none;">${formatDate(tweet.created_at)}</a></div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
showResults(userTweetsResultDiv, html);
|
|
hideUserTweetsBtn.style.display = 'inline-block';
|
|
hideUserTweetsBtn.textContent = 'Hide Results';
|
|
} else {
|
|
showSuccess(userTweetsResultDiv, `No tweets found for @${username}.`);
|
|
}
|
|
} catch (error) {
|
|
showError(userTweetsResultDiv, `Failed to get tweets for @${username}: ${error.message}`);
|
|
} finally {
|
|
getUserTweetsBtn.disabled = false;
|
|
getUserTweetsBtn.textContent = 'Get User Tweets';
|
|
}
|
|
});
|
|
|
|
// Filter Tweets Handler (Polling)
|
|
filterTweetsBtn.addEventListener('click', async () => {
|
|
const filterQuestion = filterQuestionInput.value.trim();
|
|
const summarizationQuestion = summarizationQuestionInput.value.trim();
|
|
const list = filterListInput.value.trim();
|
|
const usersText = filterUsersInput.value.trim();
|
|
|
|
if (!filterQuestion) {
|
|
showError(filterResultDiv, 'Please enter a filter question.');
|
|
return;
|
|
}
|
|
|
|
if (!summarizationQuestion) {
|
|
showError(filterResultDiv, 'Please enter a summarization question.');
|
|
return;
|
|
}
|
|
|
|
if (!list && !usersText) {
|
|
showError(filterResultDiv, 'Please enter either a list name or usernames.');
|
|
return;
|
|
}
|
|
|
|
if (list && usersText) {
|
|
showError(filterResultDiv, 'Please provide either a list name OR usernames, not both.');
|
|
return;
|
|
}
|
|
|
|
filterTweetsBtn.disabled = true;
|
|
filterTweetsBtn.textContent = 'Starting...';
|
|
|
|
// Show initial progress
|
|
showResults(filterResultDiv, '<div id="filter-progress"><p>Creating filter job...</p></div>');
|
|
|
|
try {
|
|
const requestBody = {
|
|
filter_question: filterQuestion,
|
|
summarization_question: summarizationQuestion
|
|
};
|
|
|
|
if (list) {
|
|
requestBody.list = list;
|
|
} else {
|
|
const users = usersText.split('\n').map(u => u.trim()).filter(u => u);
|
|
if (users.length === 0) {
|
|
showError(filterResultDiv, 'Please enter valid usernames.');
|
|
filterTweetsBtn.disabled = false;
|
|
filterTweetsBtn.textContent = 'Filter Tweets';
|
|
return;
|
|
}
|
|
requestBody.users = users;
|
|
}
|
|
|
|
// Create filter job
|
|
const jobResponse = await apiRequest('/filter-job', {
|
|
method: 'POST',
|
|
body: JSON.stringify(requestBody),
|
|
timeout: 60000 // 1 minute timeout for job creation
|
|
});
|
|
|
|
const jobId = jobResponse.data.job_id;
|
|
filterTweetsBtn.textContent = 'Filtering...';
|
|
showResults(filterResultDiv, '<div id="filter-progress"><p>Job created, starting polling...</p></div>');
|
|
|
|
// Initialize results container
|
|
window.currentFilterResults = null;
|
|
|
|
// Start polling for job status
|
|
await pollFilterJob(jobId);
|
|
|
|
} catch (error) {
|
|
showError(filterResultDiv, `Failed to start filtering: ${error.message}`);
|
|
filterTweetsBtn.disabled = false;
|
|
filterTweetsBtn.textContent = 'Filter Tweets';
|
|
}
|
|
});
|
|
|
|
// Function to poll filter job status
|
|
async function pollFilterJob(jobId, retryCount = 0) {
|
|
const maxRetries = 3;
|
|
const maxAttempts = 300; // 5 minute timeout
|
|
let attempts = 0;
|
|
|
|
while (attempts < maxAttempts) {
|
|
try {
|
|
const statusResponse = await apiRequest(`/filter-job/${jobId}/status`, {
|
|
timeout: 200 // 200ms timeout for status checks
|
|
});
|
|
|
|
const status = statusResponse.data;
|
|
|
|
// Update progress display and show partial results if available
|
|
if (status.progress) {
|
|
const progressHtml = `
|
|
<div id="filter-progress">
|
|
<p>${status.progress.message || 'Processing tweets'}</p>
|
|
<p style="font-size: 0.9em; color: #666;">Status: ${status.status}</p>
|
|
</div>
|
|
`;
|
|
showResults(filterResultDiv, progressHtml);
|
|
}
|
|
|
|
// Show partial results while running
|
|
if (status.status === 'running' && status.partial_results && status.partial_results.partial_tweets) {
|
|
const partialResult = {
|
|
filtered_tweets: status.partial_results.partial_tweets,
|
|
summary: null // No summary yet while running
|
|
};
|
|
|
|
// Update stored results with partial data
|
|
window.currentFilterResults = partialResult;
|
|
|
|
// Display partial results with progress indicator
|
|
displayPartialFilterResults(partialResult, status.progress);
|
|
}
|
|
|
|
if (status.status === 'completed') {
|
|
// Job completed, get final results
|
|
const resultsResponse = await apiRequest(`/filter-job/${jobId}/results`);
|
|
if (resultsResponse.data && resultsResponse.data.results) {
|
|
window.currentFilterResults = resultsResponse.data.results;
|
|
// Display final results (replacing any partial results)
|
|
displayFilterResults(window.currentFilterResults);
|
|
} else {
|
|
throw new Error('Job completed but no results available');
|
|
}
|
|
|
|
filterTweetsBtn.disabled = false;
|
|
filterTweetsBtn.textContent = 'Filter Tweets';
|
|
return;
|
|
} else if (status.status === 'failed') {
|
|
throw new Error(status.error_message || 'Job failed');
|
|
}
|
|
|
|
// Job still in progress, wait before next poll
|
|
// const delay = Math.min(1000 * Math.pow(1.5, attempts), 5000); // Exponential backoff up to 5s
|
|
const delay = 200
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
attempts++;
|
|
|
|
} catch (networkError) {
|
|
console.warn(`Network error during polling (attempt ${retryCount + 1}):`, networkError);
|
|
if (retryCount < maxRetries) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
return pollFilterJob(jobId, retryCount + 1);
|
|
} else {
|
|
throw new Error(`Network error after ${maxRetries} retries: ${networkError.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error('Job timeout after 5 minutes');
|
|
}
|
|
|
|
// Helper function to display partial filter results while running
|
|
function displayPartialFilterResults(result, progress) {
|
|
if (result && result.filtered_tweets) {
|
|
const filtered = result.filtered_tweets;
|
|
const passing = filtered.filter(item => item.pass);
|
|
const failing = filtered.filter(item => !item.pass);
|
|
|
|
let html = `<h3>Filter Results (Processing...)</h3>`;
|
|
|
|
// Show progress bar
|
|
if (progress) {
|
|
html += `
|
|
<div style="background: #f0f0f0; border-radius: 10px; overflow: hidden; margin: 10px 0;">
|
|
<div style="background: #4caf50; height: 20px; width: ${progress.percentage || 0}%; transition: width 0.3s ease;"></div>
|
|
</div>
|
|
<p style="font-size: 0.9em; color: #666; margin-bottom: 15px;">${progress.message || 'Processing tweets'}: ${progress.current || 0}/${progress.total || 0}</p>
|
|
`;
|
|
}
|
|
|
|
html += `<p><strong>Passing tweets so far:</strong> ${passing.length}</p>`;
|
|
|
|
if (passing.length > 0) {
|
|
html += '<h4 style="color: #2e7d32; margin-top: 20px;">Passing Tweets (Partial)</h4>';
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">';
|
|
|
|
passing.forEach(item => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 10px 0; margin-bottom: 10px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${item.tweet.username}</div>
|
|
<div style="margin: 5px 0; line-height: 1.4;">${item.tweet.text}</div>
|
|
<div style="font-size: 0.9em; color: #666; margin: 5px 0;"><a href="https://twitter.com/i/web/status/${item.tweet.tweet_id}" target="_blank" style="color: #1da1f2; text-decoration: none;">${formatDate(item.tweet.created_at)}</a></div>
|
|
<div style="font-size: 0.85em; color: #2e7d32; font-style: italic;">Reasoning: ${item.reasoning}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
// Show sample of failing tweets if any
|
|
if (failing.length > 0) {
|
|
html += '<h4 style="color: #d32f2f; margin-top: 20px;">Non-Passing Tweets (Partial Sample)</h4>';
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px;">';
|
|
|
|
failing.slice(0, 3).forEach(item => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${item.tweet.username}</div>
|
|
<div style="margin: 3px 0; line-height: 1.4; font-size: 0.9em;">${item.tweet.text}</div>
|
|
<div style="font-size: 0.8em; color: #d32f2f; font-style: italic;">Reasoning: ${item.reasoning}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
showResults(filterResultDiv, html);
|
|
hideFilterResultsBtn.style.display = 'inline-block';
|
|
hideFilterResultsBtn.textContent = 'Hide Results';
|
|
}
|
|
}
|
|
|
|
// Helper function to display filter results
|
|
function displayFilterResults(result) {
|
|
if (result && result.filtered_tweets) {
|
|
const filtered = result.filtered_tweets;
|
|
const passing = filtered.filter(item => item.pass);
|
|
const failing = filtered.filter(item => !item.pass);
|
|
|
|
let html = `<h3>Filter Results</h3>`;
|
|
html += `<p><strong>Passing tweets:</strong> ${passing.length}</p>`;
|
|
|
|
if (passing.length > 0) {
|
|
html += '<h4 style="color: #2e7d32; margin-top: 20px;">Passing Tweets</h4>';
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">';
|
|
|
|
passing.forEach(item => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 10px 0; margin-bottom: 10px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${item.tweet.username}</div>
|
|
<div style="margin: 5px 0; line-height: 1.4;">${item.tweet.text}</div>
|
|
<div style="font-size: 0.9em; color: #666; margin: 5px 0;"><a href="https://twitter.com/i/web/status/${item.tweet.tweet_id}" target="_blank" style="color: #1da1f2; text-decoration: none;">${formatDate(item.tweet.created_at)}</a></div>
|
|
<div style="font-size: 0.85em; color: #2e7d32; font-style: italic;">Reasoning: ${item.reasoning}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
if (failing.length > 0) {
|
|
html += '<h4 style="color: #d32f2f; margin-top: 20px;">Non-Passing Tweets</h4>';
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px;">';
|
|
|
|
failing.forEach(item => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${item.tweet.username}</div>
|
|
<div style="margin: 3px 0; line-height: 1.4; font-size: 0.9em;">${item.tweet.text}</div>
|
|
<div style="font-size: 0.8em; color: #d32f2f; font-style: italic;">Reasoning: ${item.reasoning}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
// Show sample of failing tweets if any
|
|
if (failing.length > 0) {
|
|
html += '<h4 style="color: #d32f2f; margin-top: 20px;">Non-Passing Tweets (Partial Sample)</h4>';
|
|
html += '<div style="border: 1px solid #e5e5e5; padding: 10px; border-radius: 4px;">';
|
|
|
|
failing.slice(0, 3).forEach(item => {
|
|
html += `
|
|
<div style="border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px;">
|
|
<div style="font-weight: bold; color: #1a1a1a;">@${item.tweet.username}</div>
|
|
<div style="margin: 3px 0; line-height: 1.4; font-size: 0.9em;">${item.tweet.text}</div>
|
|
<div style="font-size: 0.8em; color: #d32f2f; font-style: italic;">Reasoning: ${item.reasoning}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
// Show summary if available
|
|
if (result.summary) {
|
|
html += `<div style="background: #f8f9fa; padding: 15px; margin: 15px 0; border-radius: 4px;">`;
|
|
html += `<h3 style="margin-top: 0; color: #007bff;">Summary</h3>`;
|
|
html += `<div style="margin-bottom: 0; line-height: 1.5;">${parseMarkdown(result.summary)}</div>`;
|
|
html += `</div>`;
|
|
}
|
|
|
|
showResults(filterResultDiv, html);
|
|
hideFilterResultsBtn.style.display = 'inline-block';
|
|
hideFilterResultsBtn.textContent = 'Hide Results';
|
|
} else {
|
|
showSuccess(filterResultDiv, 'No tweets found to filter.');
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
addUsernameInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
addAccountBtn.click();
|
|
}
|
|
});
|
|
|
|
bulkUsernamesInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
bulkAddAccountsBtn.click();
|
|
}
|
|
});
|
|
|
|
newListNameInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
createListBtn.click();
|
|
}
|
|
});
|
|
|
|
listUsernamesInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
createListBtn.click();
|
|
}
|
|
});
|
|
|
|
editListNameInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
editListBtn.click();
|
|
}
|
|
});
|
|
|
|
editListUsernamesInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
editListBtn.click();
|
|
}
|
|
});
|
|
|
|
editListPasswordInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
editListBtn.click();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
userTweetsUsernameInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
getUserTweetsBtn.click();
|
|
}
|
|
});
|
|
|
|
filterQuestionInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
filterTweetsBtn.click();
|
|
}
|
|
});
|
|
|
|
summarizationQuestionInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
filterTweetsBtn.click();
|
|
}
|
|
});
|
|
|
|
filterUsersInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
filterTweetsBtn.click();
|
|
}
|
|
});
|
|
|
|
// Hide/Show All Tweets Handler
|
|
hideAllTweetsBtn.addEventListener('click', () => {
|
|
if (tweetsResultDiv.classList.contains('show')) {
|
|
tweetsResultDiv.classList.remove('show');
|
|
hideAllTweetsBtn.textContent = 'Show Results';
|
|
} else {
|
|
tweetsResultDiv.classList.add('show');
|
|
hideAllTweetsBtn.textContent = 'Hide Results';
|
|
}
|
|
});
|
|
|
|
// Hide/Show User Tweets Handler
|
|
hideUserTweetsBtn.addEventListener('click', () => {
|
|
if (userTweetsResultDiv.classList.contains('show')) {
|
|
userTweetsResultDiv.classList.remove('show');
|
|
hideUserTweetsBtn.textContent = 'Show Results';
|
|
} else {
|
|
userTweetsResultDiv.classList.add('show');
|
|
hideUserTweetsBtn.textContent = 'Hide Results';
|
|
}
|
|
});
|
|
|
|
// Hide/Show Filter Results Handler
|
|
hideFilterResultsBtn.addEventListener('click', () => {
|
|
if (filterResultDiv.classList.contains('show')) {
|
|
filterResultDiv.classList.remove('show');
|
|
hideFilterResultsBtn.textContent = 'Show Results';
|
|
} else {
|
|
filterResultDiv.classList.add('show');
|
|
hideFilterResultsBtn.textContent = 'Hide Results';
|
|
}
|
|
});
|