Files
search-hub/static/js/app.js

364 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Search Hub — Deepseek 风格 UI */
(function () {
'use strict';
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const summarizeBtn = document.getElementById('summarize-btn');
const statusBar = document.getElementById('status-bar');
const historyList = document.getElementById('history-list');
const loading = document.getElementById('loading');
const resultsSection = document.getElementById('results-section');
const emptyState = document.getElementById('empty-state');
const summarySection = document.getElementById('summary-section');
const summaryContent = document.getElementById('summary-content');
const summaryMeta = document.getElementById('summary-meta');
const searchSection = document.getElementById('search-section');
const searchTitle = document.getElementById('search-title');
let currentResults = [];
let currentQuery = '';
let currentSource = 'auto';
let isSummarizing = false;
// 初始隐藏总结区域
summarySection.style.display = 'none';
loadHistory();
setupSourceSelector();
searchBtn.addEventListener('click', doSearch);
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') doSearch();
});
summarizeBtn.addEventListener('click', doSummarize);
// ===== 搜索源选择 =====
function setupSourceSelector() {
document.querySelectorAll('.source-tag').forEach(function (el) {
el.addEventListener('click', function () {
var source = el.dataset.source;
if (!source) return;
if (el.classList.contains('source-disabled')) return;
document.querySelectorAll('.source-tag').forEach(function (t) {
t.classList.remove('source-active');
});
el.classList.add('source-active');
currentSource = source;
if (searchInput.value.trim()) {
doSearch();
}
});
});
}
// ===== 搜索(无总结) =====
function doSearch() {
const query = searchInput.value.trim();
if (!query) return;
hideSummary();
currentQuery = query;
currentResults = [];
showLoading(true);
setStatus('');
searchSection.classList.add('has-results');
searchTitle.textContent = query;
fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
source: currentSource,
}),
})
.then(function (r) { return r.json(); })
.then(function (data) {
showLoading(false);
if (data.error) {
setStatus(data.error);
resultsSection.innerHTML = '';
return;
}
currentResults = data.results || [];
renderResults(currentResults, data);
loadHistory();
})
.catch(function (err) {
showLoading(false);
setStatus('请求失败: ' + err.message);
});
}
// ===== 渲染结果 =====
function renderResults(results, data) {
if (!results || results.length === 0) {
emptyState.style.display = 'block';
resultsSection.innerHTML = '';
return;
}
emptyState.style.display = 'none';
var sourceLabel = data.source || currentSource;
var elapsed = data.elapsed || 0;
setStatus('共找到 ' + results.length + ' 条结果(' + sourceLabel + ' | ' + elapsed + 's');
document.querySelectorAll('.source-tag').forEach(function (t) {
t.classList.remove('source-active');
});
var match = document.querySelector('.source-tag[data-source="' + sourceLabel + '"]');
if (match) {
match.classList.add('source-active');
} else {
var autoTag = document.querySelector('.source-tag[data-source="auto"]');
if (autoTag) autoTag.classList.add('source-active');
}
var html = '';
results.forEach(function (item) {
var dateStr = formatDate(item.published_date || '');
var sourceTag = item.source ? '<span class="source-badge badge-ok">' + item.source + '</span>' : '';
html += '<div class="result-card">';
html += ' <div class="result-title"><a href="' + escapeHtml(item.url) + '" target="_blank" rel="noopener">' + escapeHtml(item.title) + '</a></div>';
html += ' <div class="result-content">' + escapeHtml(item.content) + '</div>';
html += ' <div class="result-meta">';
html += ' <span class="result-url">' + escapeHtml(item.url) + '</span>';
if (dateStr) html += ' <span>' + dateStr + '</span>';
html += ' ' + sourceTag;
html += ' </div>';
html += '</div>';
});
resultsSection.innerHTML = html;
}
// ===== AI 总结 =====
function doSummarize() {
if (isSummarizing) return;
// 无搜索词时取输入框内容
if (!currentQuery) {
var q = searchInput.value.trim();
if (!q) return;
currentQuery = q;
currentSource = document.querySelector('.source-tag.source-active')?.dataset.source || 'auto';
}
// 无搜索结果 → 先搜索再总结
if (!currentResults || currentResults.length === 0) {
doSearchThenSummarize();
return;
}
startSummarize();
}
function doSearchThenSummarize() {
var query = currentQuery || searchInput.value.trim();
if (!query) return;
hideSummary();
currentQuery = query;
currentResults = [];
showLoading(true);
setStatus('');
searchSection.classList.add('has-results');
searchTitle.textContent = query;
summarizeBtn.disabled = true;
summarizeBtn.textContent = '搜索中...';
fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
source: currentSource,
}),
})
.then(function (r) { return r.json(); })
.then(function (data) {
showLoading(false);
summarizeBtn.disabled = false;
summarizeBtn.textContent = 'AI 总结';
if (data.error) {
setStatus(data.error);
resultsSection.innerHTML = '';
return;
}
currentResults = data.results || [];
renderResults(currentResults, data);
loadHistory();
if (currentResults.length > 0) {
startSummarize();
}
})
.catch(function (err) {
showLoading(false);
summarizeBtn.disabled = false;
summarizeBtn.textContent = 'AI 总结';
setStatus('请求失败: ' + err.message);
});
}
function startSummarize() {
if (!currentQuery || !currentResults || currentResults.length === 0) return;
if (isSummarizing) return;
isSummarizing = true;
summaryContent.innerHTML =
'<div class="summary-input-placeholder" id="summary-placeholder">' +
' <div class="typing-indicator"><span></span><span></span><span></span></div>' +
' &nbsp;AI 总结中...' +
'</div>';
summaryMeta.textContent = '';
summarySection.style.display = 'block';
summarySection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
summarizeBtn.disabled = true;
summarizeBtn.textContent = '总结中...';
var fullSummary = '';
fetch('/api/ai-summarize/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: currentQuery,
results: currentResults,
}),
})
.then(function (r) {
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || '请求失败'); });
var reader = r.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
function read() {
return reader.read().then(function (result) {
if (result.done) return;
buffer += decoder.decode(result.value, { stream: true });
var parts = buffer.split('\n\n');
buffer = parts.pop() || '';
parts.forEach(function (part) {
var lines = part.trim().split('\n');
var eventType = '';
var dataStr = '';
lines.forEach(function (line) {
if (line.startsWith('event: ')) eventType = line.slice(7).trim();
else if (line.startsWith('data: ')) dataStr = line.slice(6).trim();
});
if (!dataStr) return;
try {
var parsed = JSON.parse(dataStr);
if (eventType === 'delta') {
fullSummary += parsed.content || '';
summaryContent.innerHTML = markedSummary(fullSummary);
} else if (eventType === 'meta') {
summaryMeta.textContent = '模型: ' + (parsed.model || '') + ' | 耗时 ' + parsed.elapsed + 's';
} else if (eventType === 'error') {
setStatus('AI 总结失败: ' + (parsed.error || ''));
}
} catch (e) {}
});
return read();
});
}
return read();
})
.then(function () {
isSummarizing = false;
summarizeBtn.disabled = false;
summarizeBtn.textContent = 'AI 总结';
})
.catch(function (err) {
isSummarizing = false;
summarizeBtn.disabled = false;
summarizeBtn.textContent = 'AI 总结';
summaryContent.innerHTML = '<div class="text-danger">总结失败: ' + escapeHtml(err.message) + '</div>';
});
}
function hideSummary() {
summarySection.style.display = 'none';
summaryContent.innerHTML = '';
summaryMeta.textContent = '';
isSummarizing = false;
}
// ===== 搜索历史 =====
function loadHistory() {
fetch('/api/history')
.then(function (r) { return r.json(); })
.then(function (data) {
var list = data.history || [];
if (list.length === 0) {
historyList.innerHTML = '';
return;
}
var html = '';
list.forEach(function (item) {
html += '<span class="history-tag">' + escapeHtml(item) + '</span>';
});
historyList.innerHTML = html;
document.querySelectorAll('.history-tag').forEach(function (el) {
el.addEventListener('click', function () {
searchInput.value = el.textContent;
doSearch();
});
});
});
}
// ===== 辅助 =====
function showLoading(v) { loading.style.display = v ? 'block' : 'none'; }
function setStatus(msg) { statusBar.textContent = msg; statusBar.style.display = msg ? 'block' : 'none'; }
function formatDate(dateStr) {
if (!dateStr) return '';
try {
var d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
} catch (e) { return dateStr; }
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
function markedSummary(text) {
if (!text) return '';
var lines = text.split('\n');
var html = '';
var inList = false;
lines.forEach(function (line) {
line = escapeHtml(line);
var hMatch = line.match(/^(#{1,3})\s+(.+)/);
if (hMatch) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h' + hMatch[1].length + '>' + hMatch[2] + '</h' + hMatch[1].length + '>';
return;
}
var lMatch = line.match(/^[\s]*[-*+]\s+(.+)/);
if (lMatch) {
if (!inList) { html += '<ul>'; inList = true; }
html += '<li>' + lMatch[1] + '</li>';
return;
}
if (line.trim() === '') {
if (inList) { html += '</ul>'; inList = false; }
return;
}
if (inList) { html += '</ul>'; inList = false; }
line = line.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html += '<p>' + line + '</p>';
});
if (inList) html += '</ul>';
return html;
}
})();