'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 (
e.stopPropagation()} > {/* Header */}

Найти фото

Поиск по доменам из whitelist'а профиля. Используется Yandex Search API.

{/* Controls */}
setQuery(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') search(); }} placeholder="Имя, событие, объект…" autoFocus />
{quota && ( <>Квота сегодня: {quota.used} / {quota.limit}{' '} {quota.remaining === 0 && (исчерпана)} )}
{profiles.find(p => p.slug === profile)?.domains?.length > 0 && (
Whitelist:{' '} {profiles.find(p => p.slug === profile).domains.slice(0, 8).join(', ')} {profiles.find(p => p.slug === profile).domains.length > 8 && '…'}
)}
{/* Body */}
{error && (
{error}
)} {meta && !error && (
Найдено всего: {meta.total} · после фильтра: {meta.filtered} · показано: {items.length} · {meta.elapsedMs} мс
)} {items.length > 0 && (
{items.map((it, i) => ( ))}
)} {items.length === 0 && !error && !searching && (
Введи запрос и нажми «Найти»
)}
{/* Footer */}
); }