rottg's picture
Update code
03f1ed6 verified
Raw
History Blame Contribute Delete
16.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search - Telegram Analytics</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">&#9776;</button>
<div class="sidebar-overlay" onclick="toggleMobileMenu()"></div>
<!-- Sidebar -->
<nav class="sidebar">
<div class="logo">
<span class="logo-icon">๐Ÿ“Š</span>
<span class="logo-text">TG Analytics</span>
</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="/" class="nav-link">
<span class="icon">๐Ÿ“ˆ</span>
<span>Overview</span>
</a>
</li>
<li class="nav-item">
<a href="/users" class="nav-link">
<span class="icon">๐Ÿ‘ฅ</span>
<span>Users</span>
</a>
</li>
<li class="nav-item">
<a href="/chat" class="nav-link">
<span class="icon">๐Ÿ’ฌ</span>
<span>Chat</span>
</a>
</li>
<li class="nav-item active">
<a href="/search" class="nav-link">
<span class="icon">๐Ÿ”</span>
<span>Search</span>
</a>
</li>
<li class="nav-item">
<a href="/ai-search" class="nav-link">
<span class="icon">๐Ÿค–</span>
<span>AI Search</span>
</a>
</li>
<li class="nav-item">
<a href="/moderation" class="nav-link">
<span class="icon">๐Ÿ›ก๏ธ</span>
<span>Moderation</span>
</a>
</li>
<li class="nav-item">
<a href="/settings" class="nav-link">
<span class="icon">โš™๏ธ</span>
<span>Settings</span>
</a>
</li>
<li class="nav-item">
<a href="/maintenance" class="nav-link">
<span class="icon">๐Ÿ”’</span>
<span>Maintenance</span>
</a>
</li>
</ul>
<div class="sidebar-footer">
<div class="export-buttons">
<button onclick="exportMessages()" class="btn btn-sm">๐Ÿ“ฅ Export Messages</button>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Header -->
<header class="header">
<h1>Search Messages</h1>
<div class="header-controls">
<select id="timeframe" class="select">
<option value="today">Today</option>
<option value="yesterday">Yesterday</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
<option value="year">This Year</option>
<option value="all" selected>All Time</option>
</select>
</div>
</header>
<!-- Search Box -->
<section class="search-box">
<input type="search" id="search-input" class="search-input"
placeholder="Search messages... (supports Hebrew and English)"
onkeypress="if(event.key === 'Enter') performSearch()">
<button onclick="performSearch()" class="btn btn-primary">๐Ÿ” Search</button>
</section>
<!-- Search Tips -->
<section class="chart-card" style="margin-bottom: var(--spacing-xl);">
<div class="chart-header">
<h3>Search Tips</h3>
</div>
<div style="padding: var(--spacing-md); color: var(--text-secondary); font-size: 0.875rem;">
<ul style="list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
<li><strong>word1 word2</strong> - Messages containing both words</li>
<li><strong>"exact phrase"</strong> - Messages with exact phrase</li>
<li><strong>word1 OR word2</strong> - Messages with either word</li>
<li><strong>word*</strong> - Prefix search (word, words, wording)</li>
<li><strong>NOT word</strong> - Exclude messages with word</li>
<li><strong>Hebrew supported</strong> - Full Hebrew text search</li>
</ul>
</div>
</section>
<!-- Search Stats -->
<section class="stats-grid" id="search-stats" style="display: none;">
<div class="stat-card">
<div class="stat-icon">๐Ÿ”</div>
<div class="stat-content">
<div class="stat-value" id="result-count">0</div>
<div class="stat-label">Results Found</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">โšก</div>
<div class="stat-content">
<div class="stat-value" id="search-time">0ms</div>
<div class="stat-label">Search Time</div>
</div>
</div>
</section>
<!-- Search Results -->
<section class="search-results" id="search-results">
<div class="empty-state">
<div class="empty-state-icon">๐Ÿ”</div>
<p>Enter a search term to find messages</p>
</div>
</section>
<!-- Pagination -->
<div class="pagination" id="pagination"></div>
</main>
<script>
// State
let currentQuery = '';
let currentPage = 1;
const pageSize = 20;
async function performSearch(page = 1) {
const query = document.getElementById('search-input').value.trim();
const timeframe = document.getElementById('timeframe').value;
if (!query) {
document.getElementById('search-results').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">๐Ÿ”</div>
<p>Enter a search term to find messages</p>
</div>
`;
document.getElementById('search-stats').style.display = 'none';
document.getElementById('pagination').innerHTML = '';
return;
}
currentQuery = query;
currentPage = page;
const resultsDiv = document.getElementById('search-results');
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
const startTime = performance.now();
try {
const offset = (page - 1) * pageSize;
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}&timeframe=${timeframe}&limit=${pageSize}&offset=${offset}`
);
const data = await response.json();
const endTime = performance.now();
const searchTime = Math.round(endTime - startTime);
// Show stats
document.getElementById('search-stats').style.display = 'grid';
document.getElementById('result-count').textContent = data.results.length +
(data.results.length === pageSize ? '+' : '');
document.getElementById('search-time').textContent = searchTime + 'ms';
if (data.results.length === 0) {
resultsDiv.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">๐Ÿ˜•</div>
<p>No messages found for "${escapeHtml(query)}"</p>
</div>
`;
document.getElementById('pagination').innerHTML = '';
return;
}
resultsDiv.innerHTML = data.results.map(result => `
<div class="search-result-item">
<div class="search-result-header">
<span class="search-result-author">${escapeHtml(result.from_name || 'Unknown')}</span>
<span class="search-result-date">${result.date}</span>
</div>
<div class="search-result-text">${highlightQuery(result.text, query)}</div>
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: var(--text-muted);">
${result.has_links ? '๐Ÿ”— Link' : ''}
${result.has_media ? '๐Ÿ–ผ๏ธ Media' : ''}
</div>
</div>
`).join('');
// Simple pagination (since we don't have total count from FTS)
renderPagination(data.results.length === pageSize);
} catch (error) {
resultsDiv.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">โŒ</div>
<p>Error performing search. Please try again.</p>
</div>
`;
}
}
function renderPagination(hasMore) {
const pagination = document.getElementById('pagination');
if (currentPage === 1 && !hasMore) {
pagination.innerHTML = '';
return;
}
let html = '';
html += `<button class="page-btn" onclick="performSearch(${currentPage - 1})"
${currentPage === 1 ? 'disabled' : ''}>&laquo; Previous</button>`;
html += `<span style="padding: 0 1rem; color: var(--text-muted);">Page ${currentPage}</span>`;
html += `<button class="page-btn" onclick="performSearch(${currentPage + 1})"
${!hasMore ? 'disabled' : ''}>Next &raquo;</button>`;
pagination.innerHTML = html;
}
function highlightQuery(text, query) {
if (!text) return '';
// Escape HTML first
text = escapeHtml(text);
// Simple highlight for each word in query
const words = query.replace(/["*]/g, '').split(/\s+/).filter(w => w && w !== 'OR' && w !== 'NOT');
words.forEach(word => {
const regex = new RegExp(`(${escapeRegex(word)})`, 'gi');
text = text.replace(regex, '<span class="search-highlight">$1</span>');
});
return text;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function exportMessages() {
const timeframe = document.getElementById('timeframe').value;
window.location.href = `/api/export/messages?timeframe=${timeframe}`;
}
// AI Search
async function aiSearch() {
const query = document.getElementById('ai-query').value.trim();
const mode = document.getElementById('ai-mode').value;
if (!query) return;
const resultDiv = document.getElementById('ai-result');
const answerDiv = document.getElementById('ai-answer');
const sqlPre = document.getElementById('ai-sql');
resultDiv.style.display = 'block';
const loadingMessages = {
'context': 'ืงื•ืจื ื”ื•ื“ืขื•ืช ื•ืžื ืชื—...',
'semantic': 'ืžื—ืคืฉ ืœืคื™ ืžืฉืžืขื•ืช + ืฉื•ืœื— ืœ-AI...',
'sql': 'ืžื—ืคืฉ...',
'auto': 'ืžื—ืคืฉ...'
};
answerDiv.textContent = loadingMessages[mode] || 'ืžื—ืคืฉ...';
sqlPre.textContent = '';
try {
const response = await fetch('/api/ai/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, mode })
});
const data = await response.json();
if (data.error) {
answerDiv.innerHTML = `<span style="color:#ff6b6b;">ืฉื’ื™ืื”: ${escapeHtml(data.error)}</span>`;
} else {
let html = escapeHtml(data.answer || 'ืœื ื ืžืฆืื” ืชืฉื•ื‘ื”');
// Show mode info
if (data.mode === 'context_search') {
html += `<br><br><small style="color:var(--text-muted);">๐Ÿง  Hybrid Search: ืงืจืืชื™ ${data.context_messages} ื”ื•ื“ืขื•ืช`;
if (data.context_user) html += ` ืž"${escapeHtml(data.context_user)}"`;
if (data.keywords_used && data.keywords_used.length > 0) {
html += `<br>๐Ÿ”‘ ืžื™ืœื•ืช ืžืคืชื—: ${data.keywords_used.slice(0, 5).join(', ')}`;
}
html += `</small>`;
} else if (data.mode === 'semantic_ai' || data.mode === 'semantic') {
html += `<br><br><small style="color:var(--text-muted);">๐Ÿ”ฎ Semantic + AI: ื ืžืฆืื• ${data.count} ื”ื•ื“ืขื•ืช ื“ื•ืžื•ืช`;
if (data.total_with_threads && data.total_with_threads > data.count) {
html += ` + ${data.total_with_threads - data.count} ื”ื•ื“ืขื•ืช ืž-threads`;
}
html += `</small>`;
}
answerDiv.innerHTML = html;
sqlPre.textContent = data.sql || '';
// If results contain messages, optionally populate main search
if (data.results && data.results.length > 0 && data.results[0].text) {
displayAIResults(data.results);
}
}
} catch (error) {
answerDiv.textContent = `ืฉื’ื™ืื”: ${error.message}`;
}
}
function displayAIResults(results) {
const resultsDiv = document.getElementById('search-results');
if (results.length === 0) return;
document.getElementById('search-stats').style.display = 'grid';
document.getElementById('result-count').textContent = results.length;
document.getElementById('search-time').textContent = 'AI';
resultsDiv.innerHTML = results.slice(0, 20).map(result => `
<div class="search-result-item">
<div class="search-result-header">
<span class="search-result-author">${escapeHtml(result.from_name || 'Unknown')}</span>
<span class="search-result-date">${result.date || ''}${result.score ? ` (${(result.score * 100).toFixed(0)}% ื“ืžื™ื•ืŸ)` : ''}</span>
</div>
<div class="search-result-text">${escapeHtml(result.text || '')}</div>
</div>
`).join('');
}
// Focus search input on page load
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('search-input').focus();
});
</script>
<script>
function toggleMobileMenu(){var s=document.querySelector('.sidebar'),o=document.querySelector('.sidebar-overlay');s.classList.toggle('open');if(o)o.classList.toggle('active');}
</script>
</body>
</html>