'use client'; import { useState, useEffect } from 'react'; import { Loader2, Search, X, AlertCircle, ExternalLink, Check } from 'lucide-react'; // Простая эвристика: если в тексте поста есть капитализованные ФИО (двусловные) — // предлагаем их как стартовый query. Иначе берём первые слова темы. function suggestQuery({ topic, post }) { const text = `${topic || ''}\n${post || ''}`.trim(); if (!text) return ''; // Имена вида «Имя Фамилия» (две заглавные кириллицей или латиницей подряд) const reName = /\b([А-ЯЁA-Z][а-яёa-z]{2,})\s+([А-ЯЁA-Z][а-яёa-z]{2,})/g; const matches = []; let m; while ((m = reName.exec(text)) !== null) { matches.push(`${m[1]} ${m[2]}`); if (matches.length >= 3) break; } if (matches.length) return matches[0]; // Иначе — первые ~6 слов темы return (topic || '').split(/\s+/).slice(0, 6).join(' '); } export default function PhotoSearchModal({ open, onClose, topic, post, onPick }) { const [profiles, setProfiles] = useState([]); const [profile, setProfile] = useState('general'); const [quota, setQuota] = useState(null); const [query, setQuery] = useState(''); const [items, setItems] = useState([]); const [searching, setSearching] = useState(false); const [error, setError] = useState(''); const [meta, setMeta] = useState(null); const [pickedIdx, setPickedIdx] = useState(null); // Загружаем профили + квоту при первом открытии useEffect(() => { if (!open) return; let cancelled = false; (async () => { try { const [profRes, quotaRes] = await Promise.all([ fetch('/api/photo-search/profiles'), fetch('/api/photo-search/quota'), ]); const profData = profRes.ok ? await profRes.json() : []; const quotaData = quotaRes.ok ? await quotaRes.json() : null; if (cancelled) return; setProfiles(profData); setQuota(quotaData); // Дефолт-профиль: general (если есть), иначе первый if (profData.length && !profData.find(p => p.slug === profile)) { setProfile(profData[0].slug); } } catch (e) { if (!cancelled) setError(e.message); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); // Подсказываем стартовый запрос useEffect(() => { if (!open) return; if (!query) setQuery(suggestQuery({ topic, post })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); async function search() { if (!query.trim() || searching) return; setSearching(true); setError(''); setItems([]); setPickedIdx(null); try { const res = await fetch('/api/photo-search/by-query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query.trim(), profile, num: 6 }), }); const data = await res.json(); if (!res.ok) { if (data.code === 'DAILY_LIMIT_EXCEEDED') { throw new Error('Дневной лимит поиска фото исчерпан. Попробуй завтра или подними лимит в /system.'); } throw new Error(data.error || `HTTP ${res.status}`); } setItems(data.items || []); setMeta({ total: data.total, raw: data.raw_count, filtered: data.filtered_count, elapsedMs: data.elapsed_ms, domains: data.domains || [], }); if (data.quota) setQuota(data.quota); if (!data.items || data.items.length === 0) { setError('Ничего не нашлось в whitelisted доменах. Попробуй другой профиль или уточни запрос.'); } } catch (e) { setError(e.message); } finally { setSearching(false); } } function pick(idx) { const item = items[idx]; if (!item) return; setPickedIdx(idx); onPick?.({ imageUrl: item.imageUrl, thumbUrl: item.thumbUrl, credit: { domain: item.sourceDomain || null, sourceUrl: item.sourceUrl || null, title: item.title || null, }, }); } if (!open) return null; return (
Поиск по доменам из whitelist'а профиля. Используется Yandex Search API.