'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings, Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart, MessageSquare, Pencil, X, Send, Clock, Search, Camera, ExternalLink, Link2 } from 'lucide-react'; import PhotoSearchModal from './PhotoSearchModal'; import PostPreview from './PostPreview'; import PostTemplates from './PostTemplates'; import ChannelAnalytics from './ChannelAnalytics'; import FromUrlModal from './FromUrlModal'; const GOAL_LABELS = { educational: 'Обучение', news: 'Новости', entertainment: 'Развлечение', expert: 'Экспертный', sales: 'Продажи', }; const TRANSFORMS = [ { action: 'shorter', label: 'Короче', icon: Scissors, desc: 'Сократить в 2 раза' }, { action: 'longer', label: 'Длиннее', icon: Maximize2, desc: 'Расширить с примерами' }, { action: 'improve', label: 'Улучшить', icon: Sparkles, desc: 'Убрать AI-штампы' }, { action: 'bolder', label: 'Дерзче', icon: Zap, desc: 'Острее формулировки' }, { action: 'softer', label: 'Мягче', icon: Heart, desc: 'Доброжелательней' }, { action: 'addCta', label: 'Призыв', icon: MessageSquare, desc: 'Добавить CTA' }, { action: 'forVk', label: 'Для ВК', icon: RefreshCw, desc: 'Адаптировать под ВКонтакте' }, ]; // Хвостовая подпись «📷 Фото: domain» — добавляется к посту, можно убрать function buildCaption(domain) { return domain ? `\n\n📷 Фото: ${domain}` : ''; } // Удаляет существующую подпись из текста поста (по любому домену) function stripCaption(text) { return (text || '').replace(/\n{1,2}📷\s*Фото:\s*[^\n]+\s*$/u, '').trimEnd(); } export default function ChannelView({ channel }) { const [topic, setTopic] = useState(''); const [generating, setGenerating] = useState(false); const [post, setPost] = useState(null); const [error, setError] = useState(''); const [copied, setCopied] = useState(false); const [tokens, setTokens] = useState(null); // Варианты постов (история) const [variants, setVariants] = useState([]); const [editing, setEditing] = useState(false); // Картинка const [image, setImage] = useState(null); const [imageCredit, setImageCredit] = useState(null); // { domain, sourceUrl, title } | null const [genImage, setGenImage] = useState(false); // Photo search modal const [showPhotoSearch, setShowPhotoSearch] = useState(false); const [showFromUrl, setShowFromUrl] = useState(false); // Трансформации const [transforming, setTransforming] = useState(false); // Идеи тем const [showIdeas, setShowIdeas] = useState(false); const [ideas, setIdeas] = useState([]); const [loadingIdeas, setLoadingIdeas] = useState(false); async function fetchIdeas() { setLoadingIdeas(true); setError(''); try { const res = await fetch('/api/generate/topics-ideas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channelId: channel.id, count: 7 }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка'); setIdeas(data.topics || []); setShowIdeas(true); } catch (err) { setError(err.message); } finally { setLoadingIdeas(false); } } // Сохранение и публикация const [savedPostId, setSavedPostId] = useState(null); const [publishing, setPublishing] = useState(false); const [activeTab, setActiveTab] = useState('generate'); // generate | analytics const [showScheduler, setShowScheduler] = useState(false); const [scheduleAt, setScheduleAt] = useState(''); const [history, setHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); useEffect(() => { loadHistory(); }, []); async function loadHistory() { setLoadingHistory(true); try { const res = await fetch(`/api/user-posts?channel_id=${channel.id}&limit=20`); const data = await res.json(); if (Array.isArray(data)) setHistory(data); } catch {} finally { setLoadingHistory(false); } } function clearImage() { setImage(null); setImageCredit(null); // Если в посте была подпись «📷 Фото: …» — убираем её при удалении фото if (post) setPost(p => stripCaption(p)); } function applyPhotoPick({ imageUrl, credit }) { setImage(imageUrl); setImageCredit(credit || null); // Подменяем (или добавляем) caption if (post && credit?.domain) { setPost(p => stripCaption(p) + buildCaption(credit.domain)); } setShowPhotoSearch(false); } function applyFromUrl({ content, imageUrl, title }) { setPost(content); if (imageUrl) setImage(imageUrl); if (title && !topic.trim()) setTopic(title.slice(0, 120)); setSavedPostId(null); } async function savePost(status = 'draft', scheduledAt = null) { if (!post) return; setPublishing(true); setError(''); try { let id = savedPostId; if (!id) { const res = await fetch('/api/user-posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channel.id, content: post, image_url: image, image_credit: imageCredit, topic: topic.trim(), status, scheduled_at: scheduledAt, }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка'); id = data.id; setSavedPostId(id); } else { const res = await fetch(`/api/user-posts/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: post, image_url: image, image_credit: imageCredit, status, scheduled_at: scheduledAt, }), }); if (!res.ok) throw new Error((await res.json()).error || 'Ошибка'); } await loadHistory(); return id; } catch (err) { setError(err.message); return null; } finally { setPublishing(false); } } async function publishNow() { const id = await savePost('draft'); if (!id) return; setPublishing(true); try { const res = await fetch(`/api/user-posts/${id}/publish`, { method: 'POST' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка'); await loadHistory(); setPost(null); setSavedPostId(null); setImage(null); setImageCredit(null); setTopic(''); } catch (err) { setError(err.message); } finally { setPublishing(false); } } async function schedule() { if (!scheduleAt) return setError('Укажите время'); const id = await savePost('scheduled', new Date(scheduleAt).toISOString()); if (!id) return; setShowScheduler(false); setScheduleAt(''); setPost(null); setSavedPostId(null); setImage(null); setImageCredit(null); setTopic(''); } function applyTemplate({ topicHint, structure }) { // Если тема не задана — подставляем подсказку if (!topic.trim()) setTopic(topicHint); // В textarea вставляем структуру как отправную точку setPost(structure); setSavedPostId(null); } async function generate(asVariant = false) { if (!topic.trim() && !asVariant) return; if (asVariant && !post) return; setGenerating(true); setError(''); const useTopic = asVariant ? `${topic} (вариант ${variants.length + 2})` : topic.trim(); try { const createRes = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'post', channelId: channel.id, topic: useTopic, useCritique: true, }), }); const job = await createRes.json(); if (!createRes.ok) throw new Error(job.error || 'Ошибка'); let final; for (let i = 0; i < 60; i++) { await new Promise(r => setTimeout(r, 2000)); const r = await fetch(`/api/generate/${job.jobId}`); const j = await r.json(); if (j.status === 'done' || j.status === 'failed') { final = j; break; } } if (!final) throw new Error('Таймаут — попробуй ещё раз'); if (final.status === 'failed') throw new Error(final.error || 'Генерация упала'); if (asVariant && post) { setVariants(v => [...v, { content: post, tokens, image, imageCredit }]); } setPost(final.result); setTokens({ in: final.tokens_in, out: final.tokens_out }); setImage(null); setImageCredit(null); } catch (err) { setError(err.message); } finally { setGenerating(false); } } async function transform(action) { if (!post || transforming) return; setTransforming(true); setError(''); try { const res = await fetch('/api/generate/transform', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channelId: channel.id, originalPost: post, action }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка'); setVariants(v => [...v, { content: post, tokens, image, imageCredit }]); setPost(data.content); setImage(null); setImageCredit(null); } catch (err) { setError(err.message); } finally { setTransforming(false); } } async function generateImage() { if (!post || genImage) return; setGenImage(true); setError(''); try { const res = await fetch('/api/generate/post-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channelId: channel.id, post }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка генерации картинки'); setImage(data.url); setImageCredit(null); // сгенерированная — без credit'а } catch (err) { setError(err.message); } finally { setGenImage(false); } } function restoreVariant(idx) { const v = variants[idx]; setVariants(arr => { const next = arr.filter((_, i) => i !== idx); next.push({ content: post, tokens, image, imageCredit }); return next; }); setPost(v.content); setTokens(v.tokens); setImage(v.image); setImageCredit(v.imageCredit || null); } async function copy() { await navigator.clipboard.writeText(post); setCopied(true); setTimeout(() => setCopied(false), 2000); } return (
К списку каналов

{channel.name}

{(channel.goal || '').split(',').map(g => GOAL_LABELS[g.trim()] || g.trim()).join(' · ')}
{channel.niche &&

{channel.niche}

}
Настройки
{/* Вкладки */}
{[['generate','Создать пост'],['analytics','Аналитика']].map(([id,label]) => ( ))}
{activeTab === 'analytics' && ( )} {activeTab === 'generate' && <> {/* Generator */}

Сгенерировать пост

{showIdeas && ideas.length > 0 && (
Идеи для постов
{ideas.map((idea, i) => ( ))}
)}