NewsAI
Digital Curator
Lịch sử tìm kiếm sẽ xuất hiện tại đây.
)}{useDailyHomeLayout ? 'Bản tin đầu ngày' : 'Kết quả tìm kiếm'}
{!useDailyHomeLayout && (Đang phân tích và tìm bài viết...
Nhập từ khóa hoặc đường dẫn bài báo để bắt đầu.
import { useEffect, useRef, useState } from 'react' import SearchBox from './components/SearchBox' import FilterBar from './components/FilterBar' import NewsArticle from './components/NewsArticle' import SummaryBox from './components/SummaryBox' import HomeNewsGrid from './components/HomeNewsGrid' import apiService from './services/api.service' import './App.css' const HISTORY_KEY = 'newsai-search-history' const DAILY_SNAPSHOT_KEY = 'newsai-daily-snapshot' const MAX_HISTORY_ITEMS = 6 const SUMMARY_TTS_STORAGE_KEY = 'newsai-summary-tts-jobs' const ARTICLE_TTS_STORAGE_KEY = 'newsai-article-tts-jobs' const ACTIVE_TTS_STATUSES = new Set(['queued', 'processing']) const createIdleTtsState = () => ({ key: '', status: 'idle', audioUrl: '', error: '', createdAt: '', updatedAt: '', }) const normalizeTtsState = (value) => { if (!value || typeof value !== 'object') return createIdleTtsState() return { key: typeof value.key === 'string' ? value.key : '', status: typeof value.status === 'string' ? value.status : 'idle', audioUrl: typeof value.audioUrl === 'string' ? value.audioUrl : '', error: typeof value.error === 'string' ? value.error : '', createdAt: typeof value.createdAt === 'string' ? value.createdAt : '', updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : '', } } const normalizeTtsStateMap = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value)) return {} return Object.entries(value).reduce((accumulator, [key, state]) => { if (!key || typeof key !== 'string') return accumulator accumulator[key] = normalizeTtsState(state) return accumulator }, {}) } const loadTtsStateMapFromStorage = (storageKey) => { if (typeof window === 'undefined') return {} try { const raw = window.localStorage.getItem(storageKey) if (!raw) return {} const parsed = JSON.parse(raw) return normalizeTtsStateMap(parsed) } catch { return {} } } const saveTtsStateMapToStorage = (storageKey, value) => { if (typeof window === 'undefined') return try { window.localStorage.setItem(storageKey, JSON.stringify(value)) } catch { // Ignore storage quota and browser privacy mode errors. } } const getSummarySignature = (text = '', voice = '') => (text.trim() + voice).toLowerCase().replace(/\s+/g, ' ').slice(0, 280) const getArticleTtsSlot = (articleUrl, index) => { if (typeof articleUrl === 'string' && articleUrl.trim() && articleUrl !== '#') { return articleUrl } return `local-article-${index}` } const toTtsStateFromJob = (job, fallback = createIdleTtsState()) => { return { key: typeof job?.key === 'string' ? job.key : fallback.key, status: typeof job?.status === 'string' ? job.status : fallback.status, audioUrl: typeof job?.audioUrl === 'string' ? job.audioUrl : fallback.audioUrl, error: typeof job?.error === 'string' ? job.error : '', createdAt: typeof job?.createdAt === 'string' ? job.createdAt : fallback.createdAt, updatedAt: typeof job?.updatedAt === 'string' ? job.updatedAt : fallback.updatedAt, } } const isSameTtsState = (left, right) => { if (!left && !right) return true if (!left || !right) return false return ( left.key === right.key && left.status === right.status && left.audioUrl === right.audioUrl && left.error === right.error && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt ) } const getTtsErrorMessage = (error, fallbackMessage) => { return error?.response?.data?.error || error?.response?.data?.details || error?.message || fallbackMessage } const getLocalDayKey = (date = new Date()) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const formatDateForQuery = (date = new Date()) => { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}/${month}/${year}`; }; const getDailyAutoQuery = (date = new Date()) => `tin tức việt nam ${formatDateForQuery(date)}`; const createHistoryId = () => { if (globalThis.crypto?.randomUUID) { return globalThis.crypto.randomUUID(); } return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }; const normalizeHistoryEntry = (entry, index) => { if (typeof entry === 'string') { const query = entry.trim(); if (!query) return null; return { id: `legacy-${index}-${query.toLowerCase().replace(/\s+/g, '-')}`, query, createdAt: new Date(0).toISOString(), dayKey: '', autoDaily: false, filters: { voice: 'Bắc', time: 'pd', source: 'all', }, articles: [], summary: '', summaryReady: false, totalArticles: 0, }; } if (!entry || typeof entry !== 'object') return null; const query = typeof entry.query === 'string' ? entry.query.trim() : ''; if (!query) return null; return { id: typeof entry.id === 'string' && entry.id ? entry.id : createHistoryId(), query, createdAt: typeof entry.createdAt === 'string' ? entry.createdAt : new Date().toISOString(), dayKey: typeof entry.dayKey === 'string' ? entry.dayKey : '', autoDaily: Boolean(entry.autoDaily), filters: { voice: entry.filters?.voice || 'Bắc', time: entry.filters?.time || 'pd', source: entry.filters?.source || 'all', }, articles: Array.isArray(entry.articles) ? entry.articles : [], summary: typeof entry.summary === 'string' ? entry.summary : '', summaryReady: typeof entry.summaryReady === 'boolean' ? entry.summaryReady : Boolean(typeof entry.summary === 'string' && entry.summary.trim().length > 0), totalArticles: Number.isFinite(entry.totalArticles) ? entry.totalArticles : 0, }; }; const loadHistoryFromStorage = () => { if (typeof window === 'undefined') return []; try { const rawHistory = window.localStorage.getItem(HISTORY_KEY); if (!rawHistory) return []; const parsedHistory = JSON.parse(rawHistory); if (!Array.isArray(parsedHistory)) return []; return parsedHistory .map((entry, index) => normalizeHistoryEntry(entry, index)) .filter((entry) => entry && !entry.autoDaily) .slice(0, MAX_HISTORY_ITEMS); } catch { return []; } }; const loadDailySnapshotFromStorage = () => { if (typeof window === 'undefined') return null; try { const rawSnapshot = window.localStorage.getItem(DAILY_SNAPSHOT_KEY); if (!rawSnapshot) return null; const parsedSnapshot = JSON.parse(rawSnapshot); const normalizedSnapshot = normalizeHistoryEntry(parsedSnapshot, 0); if (!normalizedSnapshot) return null; return { ...normalizedSnapshot, autoDaily: true, }; } catch { return null; } }; const saveDailySnapshotToStorage = (entry) => { if (typeof window === 'undefined') return; try { if (!entry) { window.localStorage.removeItem(DAILY_SNAPSHOT_KEY); return; } window.localStorage.setItem(DAILY_SNAPSHOT_KEY, JSON.stringify(entry)); } catch { // Ignore storage quota and browser privacy mode errors. } }; function App() { const [loading, setLoading] = useState(false); const [summaryLoading, setSummaryLoading] = useState(false); const [error, setError] = useState(''); const [articles, setArticles] = useState([]); const [summary, setSummary] = useState(null); const [totalArticles, setTotalArticles] = useState(0); // Individual article summaries const [articleSummaries, setArticleSummaries] = useState({}); const [articleSummaryLoading, setArticleSummaryLoading] = useState({}); // Filter states const [voice, setVoice] = useState('Bắc'); const [time, setTime] = useState('pd'); const [source, setSource] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [searchHistory, setSearchHistory] = useState([]); const [historyReady, setHistoryReady] = useState(false); const [summaryTtsJobs, setSummaryTtsJobs] = useState(() => loadTtsStateMapFromStorage(SUMMARY_TTS_STORAGE_KEY)); const [articleTtsJobs, setArticleTtsJobs] = useState(() => loadTtsStateMapFromStorage(ARTICLE_TTS_STORAGE_KEY)); const [isMobileSummaryOpen, setIsMobileSummaryOpen] = useState(false); const [useDailyHomeLayout, setUseDailyHomeLayout] = useState(true); const hasBootstrappedRef = useRef(false); const handleSearchRef = useRef(() => {}); const closeMobileSummaryPanel = () => { setIsMobileSummaryOpen(false); }; const toggleMobileSummaryPanel = () => { setIsMobileSummaryOpen((previous) => !previous); }; useEffect(() => { if (!historyReady || typeof window === 'undefined') return; try { const manualHistory = searchHistory.filter((entry) => !entry.autoDaily); window.localStorage.setItem(HISTORY_KEY, JSON.stringify(manualHistory)); } catch { // Ignore storage quota and browser privacy mode errors. } }, [searchHistory, historyReady]); useEffect(() => { saveTtsStateMapToStorage(SUMMARY_TTS_STORAGE_KEY, summaryTtsJobs); }, [summaryTtsJobs]); useEffect(() => { saveTtsStateMapToStorage(ARTICLE_TTS_STORAGE_KEY, articleTtsJobs); }, [articleTtsJobs]); useEffect(() => { const pendingSummaryJobs = Object.entries(summaryTtsJobs).filter( ([, state]) => state.key && ACTIVE_TTS_STATUSES.has(state.status) ); const pendingArticleJobs = Object.entries(articleTtsJobs).filter( ([, state]) => state.key && ACTIVE_TTS_STATUSES.has(state.status) ); if (pendingSummaryJobs.length === 0 && pendingArticleJobs.length === 0) { return; } let canceled = false; let pollTimer = null; const fetchJobState = async (state) => { try { const job = await apiService.getTtsJob(state.key); return toTtsStateFromJob(job, state); } catch (error) { if (error?.response?.status === 404) { return { ...state, status: 'failed', error: 'Không tìm thấy tiến trình TTS trên server.', }; } return state; } }; const pollPendingJobs = async () => { const [summaryUpdates, articleUpdates] = await Promise.all([ Promise.all( pendingSummaryJobs.map(async ([slot, state]) => { const nextState = await fetchJobState(state); return [slot, nextState]; }) ), Promise.all( pendingArticleJobs.map(async ([slot, state]) => { const nextState = await fetchJobState(state); return [slot, nextState]; }) ), ]); if (canceled) return; if (summaryUpdates.length > 0) { setSummaryTtsJobs((previous) => { let changed = false; const next = { ...previous }; summaryUpdates.forEach(([slot, nextState]) => { const currentState = previous[slot]; if (!currentState || isSameTtsState(currentState, nextState)) return; next[slot] = normalizeTtsState(nextState); changed = true; }); return changed ? next : previous; }); } if (articleUpdates.length > 0) { setArticleTtsJobs((previous) => { let changed = false; const next = { ...previous }; articleUpdates.forEach(([slot, nextState]) => { const currentState = previous[slot]; if (!currentState || isSameTtsState(currentState, nextState)) return; next[slot] = normalizeTtsState(nextState); changed = true; }); return changed ? next : previous; }); } pollTimer = window.setTimeout(pollPendingJobs, 2200); }; pollPendingJobs(); return () => { canceled = true; if (pollTimer) { window.clearTimeout(pollTimer); } }; }, [summaryTtsJobs, articleTtsJobs]); const mapBraveResultToArticle = (result) => { // Helper function to strip HTML tags const stripHtml = (html) => { if (!html) return ''; const tmp = document.createElement('DIV'); tmp.innerHTML = html; return tmp.textContent || tmp.innerText || ''; }; // Determine category color based on content or default // Map từ Brave subtype sang UI style const SUBTYPE_MAPPING = { // Tin tức / Bài viết chung article: { name: 'Tin tức', tone: 'news' }, news: { name: 'Thời sự', tone: 'news' }, // Mua sắm / Sản phẩm product: { name: 'Mua sắm', tone: 'commerce' }, // Ẩm thực / Công thức recipe: { name: 'Ẩm thực', tone: 'culture' }, // Hỏi đáp / Diễn đàn qa: { name: 'Hỏi đáp', tone: 'discussion' }, discussion: { name: 'Thảo luận', tone: 'discussion' }, // Đánh giá / Review review: { name: 'Review', tone: 'review' }, // Video (Cái này thường nằm ở type 'video_result' nhưng map luôn cho chắc) video_result: { name: 'Video', tone: 'video' }, // Phim ảnh movie: { name: 'Phim ảnh', tone: 'culture' }, // Mặc định (generic) generic: { name: 'Kết quả', tone: 'generic' }, }; const getCategoryStyle = (subtype) => { const key = subtype?.toLowerCase() || 'generic'; return SUBTYPE_MAPPING[key] || SUBTYPE_MAPPING.generic; }; const style = getCategoryStyle(result.subtype); return { category: style.name, categoryTone: style.tone, source: result.profile?.name || result.meta_url?.hostname || 'Unknown', timeAgo: result.age || "", title: stripHtml(result.title) || 'Không có tiêu đề', description: stripHtml(result.description) || '', imageUrl: result.thumbnail?.src || result.thumbnail?.original || 'https://placehold.co/400x300?text=No+Image', imageAlt: stripHtml(result.title) || 'News image', articleUrl: result.url || '#' }; }; const applyHistorySnapshot = (entry) => { setError(''); setLoading(false); setSummaryLoading(false); setArticleSummaries({}); setArticleSummaryLoading({}); setArticles(Array.isArray(entry.articles) ? entry.articles : []); setSummary(entry.summary || null); setTotalArticles(entry.totalArticles || entry.articles?.length || 0); }; const upsertHistoryEntry = ({ query, articles: nextArticles, summary: nextSummary, totalCount, autoDaily = false }) => { const normalizedQuery = query.trim(); if (!normalizedQuery || !Array.isArray(nextArticles) || nextArticles.length === 0) return; const entry = { id: createHistoryId(), query: normalizedQuery, createdAt: new Date().toISOString(), dayKey: getLocalDayKey(), autoDaily, filters: { voice, time, source, }, articles: nextArticles, summary: nextSummary || '', summaryReady: Boolean(nextSummary && nextSummary.trim().length > 0), totalArticles: totalCount || nextArticles.length, }; if (autoDaily) { saveDailySnapshotToStorage(entry); return; } setSearchHistory((previous) => { return [entry, ...previous].slice(0, MAX_HISTORY_ITEMS); }); }; const handleHistorySelect = (entry) => { if (!entry) return; const selectedEntry = searchHistory.find((item) => item.id === entry.id) || entry; const hasSnapshot = (selectedEntry.articles?.length || 0) > 0 || Boolean(selectedEntry.summary); const shouldUseHomeLayout = Boolean(selectedEntry.autoDaily && selectedEntry.dayKey === getLocalDayKey()); setUseDailyHomeLayout(shouldUseHomeLayout); setSearchQuery(selectedEntry.query || ''); setVoice(selectedEntry.filters?.voice || 'Bắc'); setTime(selectedEntry.filters?.time || 'pd'); setSource(selectedEntry.filters?.source || 'all'); closeMobileSummaryPanel(); if (hasSnapshot) { applyHistorySnapshot(selectedEntry); setSearchHistory((previous) => { const filtered = previous.filter((item) => item.id !== selectedEntry.id); return [selectedEntry, ...filtered].slice(0, MAX_HISTORY_ITEMS); }); return; } handleSearch(selectedEntry.query, { autoDaily: false }); }; const handleArticleSummarize = async (articleUrl, index) => { const currentSummary = articleSummaries[index]; // If summary exists, toggle visibility if (currentSummary?.content) { setArticleSummaries(prev => ({ ...prev, [index]: { ...prev[index], visible: !prev[index].visible } })); return; } // If no summary yet, fetch it setArticleSummaryLoading(prev => ({ ...prev, [index]: true })); const articleTtsSlot = getArticleTtsSlot(articleUrl, index); try { const data = await apiService.scrape(articleUrl); const summaryData = await apiService.summarizeNews(data.text, data.title); setArticleSummaries(prev => ({ ...prev, [index]: { content: summaryData.summary, visible: true } })); setArticleTtsJobs((previous) => { if (!previous[articleTtsSlot]) return previous; const next = { ...previous }; delete next[articleTtsSlot]; return next; }); } catch (err) { console.error('Article summarize error:', err); setArticleSummaries(prev => ({ ...prev, [index]: { content: 'Không thể tóm tắt bài viết này: ' + (err.response?.data?.error || err.message), visible: true } })); } finally { setArticleSummaryLoading(prev => ({ ...prev, [index]: false })); } }; const handleGenerateSummaryTts = async () => { const summaryText = typeof summary === 'string' ? summary.trim() : ''; if (!summaryText) return; const signature = getSummarySignature(summaryText, voice); if (!signature) return; const currentState = summaryTtsJobs[signature] || createIdleTtsState(); if (ACTIVE_TTS_STATUSES.has(currentState.status)) return; const VOICE_MAP = { 'Bắc': 'nam_bac.wav', 'Nam': 'nu_nam.wav', 'Trung': 'nam_trung.wav' }; setSummaryTtsJobs((previous) => ({ ...previous, [signature]: { ...normalizeTtsState(previous[signature]), status: 'queued', audioUrl: '', error: '', }, })); try { const speakerAudio = VOICE_MAP[voice] || 'nam_bac.wav'; const createdJob = await apiService.createTtsJob(summaryText, { language: 'vi', speakerAudio }); if (!createdJob?.key) { throw new Error('Không nhận được key TTS từ server.'); } setSummaryTtsJobs((previous) => ({ ...previous, [signature]: { ...normalizeTtsState(previous[signature]), key: createdJob.key, status: createdJob.status || 'queued', createdAt: createdJob.createdAt || previous[signature]?.createdAt || '', error: '', }, })); } catch (err) { setSummaryTtsJobs((previous) => ({ ...previous, [signature]: { ...normalizeTtsState(previous[signature]), status: 'failed', error: getTtsErrorMessage(err, 'Không thể tạo audio cho bản tóm tắt.'), }, })); } }; const handleGenerateArticleTts = async (articleUrl, index) => { const summaryText = articleSummaries[index]?.content?.trim(); const slot = getArticleTtsSlot(articleUrl, index); if (!summaryText || summaryText.startsWith('Không thể tóm tắt')) { setArticleTtsJobs((previous) => ({ ...previous, [slot]: { ...normalizeTtsState(previous[slot]), status: 'failed', error: 'Hãy tạo bản tóm tắt hợp lệ trước khi chuyển thành giọng nói.', audioUrl: '', }, })); return; } const currentState = articleTtsJobs[slot] || createIdleTtsState(); if (ACTIVE_TTS_STATUSES.has(currentState.status)) return; const VOICE_MAP = { 'Bắc': 'nam_bac.wav', 'Nam': 'nu_nam.wav', 'Trung': 'nam_trung.wav' }; try { const speakerAudio = VOICE_MAP[voice] || 'nam_bac.wav'; const createdJob = await apiService.createTtsJob(summaryText, { language: 'vi', speakerAudio }); if (!createdJob?.key) { throw new Error('Không nhận được key TTS từ server.'); } setArticleTtsJobs((previous) => ({ ...previous, [slot]: { ...normalizeTtsState(previous[slot]), key: createdJob.key, status: createdJob.status || 'queued', createdAt: createdJob.createdAt || previous[slot]?.createdAt || '', error: '', }, })); } catch (err) { setArticleTtsJobs((previous) => ({ ...previous, [slot]: { ...normalizeTtsState(previous[slot]), status: 'failed', error: getTtsErrorMessage(err, 'Không thể tạo audio cho bài viết này.'), }, })); } }; const handleDailyCardSummarize = async (articleUrl) => { if (!articleUrl || articleUrl === '#') return; setError(''); setSummaryLoading(true); try { const data = await apiService.scrape(articleUrl); const summaryData = await apiService.summarizeNews(data.text, data.title); setSummary(summaryData.summary || ''); setTotalArticles(1); setIsMobileSummaryOpen(true); } catch (err) { setError('Không thể tóm tắt bài viết này: ' + (err.response?.data?.error || err.message)); } finally { setSummaryLoading(false); } }; const handleSearch = async (rawQuery = searchQuery, options = {}) => { const { autoDaily = false } = options; const query = rawQuery.trim(); if (!query) return; closeMobileSummaryPanel(); setUseDailyHomeLayout(Boolean(autoDaily)); setSearchQuery(query); setLoading(true); setSummaryLoading(false); setError(''); setSummary(null); setTotalArticles(0); setArticles([]); setArticleSummaries({}); setArticleSummaryLoading({}); const isUrl = /^https?:\/\//i.test(query); let nextArticles = []; let nextSummary = ''; let nextTotalArticles = 0; try { if (isUrl) { // Scrape the URL and summarize const data = await apiService.scrape(query); console.log('Scraped data:', data); // Create article from scraped content const article = { category: "Tin tức", categoryTone: 'news', source: new URL(query).hostname, timeAgo: "Vừa xong", title: data.title || 'Không có tiêu đề', description: data.text?.substring(0, 200) + '...' || '', imageUrl: 'https://placehold.co/400x300?text=No+Image', imageAlt: data.title || 'Scraped content', articleUrl: query }; nextArticles = [article]; setArticles(nextArticles); setLoading(false); setSummaryLoading(true); // Tóm tắt bài báo đơn lẻ try { const summaryData = await apiService.summarizeNews(data.text, data.title); nextSummary = summaryData.summary || ''; nextTotalArticles = 1; setSummary(nextSummary || null); setTotalArticles(1); } catch (err) { console.error('Summarize error:', err); setError('Không thể tóm tắt bài viết này.'); } finally { setSummaryLoading(false); } } else { // Search first to show articles immediately const searchOptions = { freshness: time, language: source === 'all' ? 'vi' : source }; console.log('Searching...'); const searchData = await apiService.search(query, searchOptions); console.log('Search results:', searchData); // Check for family_friendly at the top level if (searchData.web?.family_friendly === false || searchData.news?.family_friendly === false) { setError('Kết quả tìm kiếm chứa nội dung không phù hợp. Vui lòng thử từ khóa khác.'); setArticles([]); setLoading(false); return; } // Map Brave results to articles and show immediately let urls = []; let hasUnsafeContent = false; if (searchData.news?.results && searchData.news.results.length > 0) { // Check for unsafe content const unsafeResults = searchData.news.results.filter(r => r.family_friendly === false); if (unsafeResults.length > 0) { hasUnsafeContent = true; } const mappedArticles = searchData.news.results .filter(result => { // Loại bỏ nội dung không phù hợp if (result.family_friendly === false) { return false; } // Loại bỏ video và image results const isVideoType = result.type === 'video_result' || result.subtype === 'video'; const hasVideo = result.video !== undefined; const isImage = result.type === 'image_result'; const isVideoUrl = result.url?.includes('youtube.com') || result.url?.includes('tiktok.com') || result.url?.includes('youtu.be'); return !isVideoType && !hasVideo && !isImage && !isVideoUrl; }) .slice(0, 10) .map((result) => mapBraveResultToArticle(result)); // Check if all results were filtered out if (mappedArticles.length === 0) { if (hasUnsafeContent) { setError('Kết quả tìm kiếm chứa nội dung không phù hợp. Vui lòng thử từ khóa khác.'); } else { setError('Không tìm thấy kết quả nào'); } setArticles([]); setLoading(false); return; } nextArticles = mappedArticles; setArticles(nextArticles); urls = searchData.news.results .filter(result => { if (result.family_friendly === false) { return false; } const isVideoType = result.type === 'video_result' || result.subtype === 'video'; const hasVideo = result.video !== undefined; const isImage = result.type === 'image_result'; const isVideoUrl = result.url?.includes('youtube.com') || result.url?.includes('tiktok.com') || result.url?.includes('youtu.be'); return !isVideoType && !hasVideo && !isImage && !isVideoUrl; }) .slice(0, 10) .map(r => r.url); } else if (searchData.web?.results && searchData.web.results.length > 0) { // Check for unsafe content const unsafeResults = searchData.web.results.filter(r => r.family_friendly === false); if (unsafeResults.length > 0) { hasUnsafeContent = true; } const mappedArticles = searchData.web.results .filter(result => { // Loại bỏ nội dung không phù hợp if (result.family_friendly === false) { return false; } // Chỉ lấy search_result thông thường, bỏ video/image const isSearchResult = result.type === 'search_result'; const isVideoType = result.subtype === 'video'; const hasVideo = result.video !== undefined; const isImage = result.type === 'image_result'; const isVideoUrl = result.url?.includes('youtube.com') || result.url?.includes('tiktok.com') || result.url?.includes('youtu.be'); return isSearchResult && !isVideoType && !hasVideo && !isImage && !isVideoUrl; }) .slice(0, 10) .map((result) => mapBraveResultToArticle(result)); // Check if all results were filtered out if (mappedArticles.length === 0) { if (hasUnsafeContent) { setError('Kết quả tìm kiếm chứa nội dung không phù hợp. Vui lòng thử từ khóa khác.'); } else { setError('Không tìm thấy kết quả nào'); } setArticles([]); setLoading(false); return; } nextArticles = mappedArticles; setArticles(nextArticles); urls = searchData.web.results .filter(result => { if (result.family_friendly === false) { return false; } const isSearchResult = result.type === 'search_result'; const isVideoType = result.subtype === 'video'; const hasVideo = result.video !== undefined; const isImage = result.type === 'image_result'; const isVideoUrl = result.url?.includes('youtube.com') || result.url?.includes('tiktok.com') || result.url?.includes('youtu.be'); return isSearchResult && !isVideoType && !hasVideo && !isImage && !isVideoUrl; }) .slice(0, 10) .map(r => r.url); } else { setError('Không tìm thấy kết quả nào'); setArticles([]); setLoading(false); return; } // Now summarize in background setLoading(false); // End search loading setSummaryLoading(true); // Start summary loading try { console.log('Summarizing articles...'); const data = await apiService.scrapeAndSummarize(urls, query); console.log('Summary results:', data); if (data.summary) { nextSummary = data.summary; nextTotalArticles = data.totalArticles || nextArticles.length; setSummary(nextSummary); setTotalArticles(nextTotalArticles); } } catch (err) { console.error('Summarize error:', err); setError('Không thể tóm tắt: ' + (err.response?.data?.error || err.message)); } finally { setSummaryLoading(false); } } if (nextArticles.length > 0) { upsertHistoryEntry({ query, articles: nextArticles, summary: nextSummary, totalCount: nextTotalArticles || nextArticles.length, autoDaily, }); } } catch (err) { setError(err.response?.data?.error || 'Đã xảy ra lỗi khi xử lý yêu cầu'); console.error('Error:', err); setLoading(false); setSummaryLoading(false); } finally { // Ensure loading is stopped if (isUrl) { setLoading(false); } } }; const handleDailyResummary = async () => { if (loading || summaryLoading) return; const urls = articles .map((article) => article.articleUrl) .filter((url) => typeof url === 'string' && /^https?:\/\//i.test(url)); if (urls.length === 0) { setError('Không có bài viết hợp lệ để tóm tắt lại.'); return; } const dailyQuery = getDailyAutoQuery(); setError(''); setSummaryLoading(true); try { const data = await apiService.scrapeAndSummarize(urls, dailyQuery); const nextSummary = data.summary || ''; if (!nextSummary.trim()) { setError('Không nhận được bản tóm tắt mới. Vui lòng thử lại.'); return; } const nextTotalArticles = data.totalArticles || urls.length; setSummary(nextSummary); setTotalArticles(nextTotalArticles); upsertHistoryEntry({ query: dailyQuery, articles, summary: nextSummary, totalCount: nextTotalArticles, autoDaily: true, }); } catch (err) { setError('Không thể tóm tắt lại: ' + (err.response?.data?.error || err.message)); } finally { setSummaryLoading(false); } }; handleSearchRef.current = handleSearch; const summarySignature = getSummarySignature(summary || ''); const summaryTtsState = summarySignature ? normalizeTtsState(summaryTtsJobs[summarySignature]) : createIdleTtsState(); // Bootstrap cache on first render: // 1) Load manual search history for sidebar // 2) Load today's daily snapshot from dedicated storage // 3) Otherwise trigger daily auto-search once useEffect(() => { if (hasBootstrappedRef.current) return; hasBootstrappedRef.current = true; const persistedHistory = loadHistoryFromStorage(); const persistedDailySnapshot = loadDailySnapshotFromStorage(); setSearchHistory(persistedHistory); setHistoryReady(true); const todayKey = getLocalDayKey(); const dailyAutoQuery = getDailyAutoQuery(); const todayAutoEntry = persistedDailySnapshot && persistedDailySnapshot.dayKey === todayKey && persistedDailySnapshot.articles.length > 0 && persistedDailySnapshot.summaryReady ? persistedDailySnapshot : null; if (todayAutoEntry) { setSearchQuery(dailyAutoQuery); setUseDailyHomeLayout(true); setVoice(todayAutoEntry.filters?.voice || 'Bắc'); setTime(todayAutoEntry.filters?.time || 'pd'); setSource(todayAutoEntry.filters?.source || 'all'); applyHistorySnapshot(todayAutoEntry); return; } setSearchQuery(dailyAutoQuery); handleSearchRef.current(dailyAutoQuery, { autoDaily: true }); }, []); return (
Digital Curator
Lịch sử tìm kiếm sẽ xuất hiện tại đây.
)}Đang phân tích và tìm bài viết...
Nhập từ khóa hoặc đường dẫn bài báo để bắt đầu.