Files
Ник (Claude) 5bf01ec394 feat: drafts UI — /drafts review page + batch generate button
/drafts page: список черновиков по статусам (pending/approved/rejected)
  Одобрить + выбрать время → scheduled_post в календарь
  Редактировать текст inline, отклонить, удалить
Header: ссылка 'Черновики' (FileText иконка)
ChannelView: кнопка 'Авто ×N' для batch-генерации (async)
ChannelEdit AI-стиль: секция авто-черновиков (toggle + count + time)
API routes: /api/drafts, /api/drafts/[id]/{approve,reject}
  /api/channels/[channelId]/drafts/generate
2026-06-12 23:48:17 +03:00

608 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Save, Trash2, Loader2, Image as ImageIcon, Type, Palette, Plus, X, Sparkles, Plug } from 'lucide-react';
import TopicBank from './TopicBank';
const GOALS = [
{ v: 'educational', label: 'Обучение', desc: 'Объясняем, разбираем' },
{ v: 'news', label: 'Новости', desc: 'Что произошло' },
{ v: 'entertainment', label: 'Развлечение', desc: 'Лёгкий контент, мемы' },
{ v: 'expert', label: 'Экспертный', desc: 'Глубокий анализ, инсайты' },
{ v: 'sales', label: 'Продажи', desc: 'Подвести к покупке' },
];
const TONES = [
{ v: 'friendly', label: 'Дружелюбный' },
{ v: 'serious', label: 'Серьёзный' },
{ v: 'ironic', label: 'Ироничный' },
{ v: 'provocative', label: 'Провокационный' },
{ v: 'academic', label: 'Академичный' },
];
const LENGTHS = [
{ v: 'short', label: 'Короткий', desc: 'до 300' },
{ v: 'medium', label: 'Средний', desc: '300-800' },
{ v: 'long', label: 'Длинный', desc: '800-2000' },
];
const HUMOR = [
{ v: 'none', label: 'Без юмора' },
{ v: 'dry', label: 'Сухой/ирония' },
{ v: 'moderate', label: 'Умеренный' },
{ v: 'playful', label: 'Игривый' },
];
const EMOJI = [
{ v: 'none', label: 'Без эмодзи' },
{ v: 'moderate', label: 'Умеренно' },
{ v: 'active', label: 'Активно' },
];
const IMAGE_STYLES = [
{ v: 'realistic-photo', label: 'Реалистичное фото', desc: 'AI-фотореализм, не сток' },
{ v: 'flat-illustration',label: 'Плоская иллюстрация', desc: 'Editorial vector' },
{ v: '3d-render', label: '3D рендер', desc: 'Pixar-like' },
{ v: 'cartoon', label: 'Мультяшный', desc: 'Comic book' },
{ v: 'minimal', label: 'Минимализм', desc: 'Один элемент' },
{ v: 'abstract', label: 'Абстракция', desc: 'Геометрия, настроение' },
{ v: 'sketch', label: 'Скетч', desc: 'Карандашный рисунок' },
{ v: 'cyberpunk', label: 'Киберпанк', desc: 'Неон, будущее' },
];
const IMAGE_PALETTES = [
{ v: 'auto', label: 'Авто' },
{ v: 'dark', label: 'Тёмная' },
{ v: 'light', label: 'Светлая' },
{ v: 'warm', label: 'Тёплая' },
{ v: 'cool', label: 'Холодная' },
{ v: 'mono', label: 'Монохром' },
{ v: 'vibrant', label: 'Яркая' },
];
const TABS = [
{ id: 'content', label: 'Контент', icon: Type },
{ id: 'images', label: 'Картинки', icon: ImageIcon },
{ id: 'ai', label: 'AI-стиль', icon: Sparkles },
{ id: 'connect', label: 'Подключение', icon: Plug },
];
export default function ChannelEdit({ channel }) {
const router = useRouter();
const style = channel.style || {};
const [tab, setTab] = useState('content');
// Контент
const [name, setName] = useState(channel.name || '');
const [niche, setNiche] = useState(channel.niche || '');
const [audience, setAudience] = useState(channel.audience || '');
const [goals, setGoals] = useState(
channel.goal ? channel.goal.split(',').map(g => g.trim()).filter(Boolean) : ['educational']
);
const [customGoal, setCustomGoal] = useState('');
const [language, setLanguage] = useState(channel.language || 'ru');
const [tone, setTone] = useState(style.tone || 'friendly');
const [formality, setFormality] = useState(style.formality || 'informal');
const [humor, setHumor] = useState(style.humor || 'moderate');
const [postLength, setPostLength] = useState(style.post_length || 'medium');
const [emojiLevel, setEmojiLevel] = useState(style.emoji_level || 'moderate');
const [hashtagsMode, setHashtagsMode] = useState(style.hashtags_mode || 'end');
const [bannedWords, setBannedWords] = useState((style.banned_words || []).join(', '));
const [bannedTopics, setBannedTopics] = useState((style.banned_topics || []).join(', '));
// Картинки
const [imageEnabled, setImageEnabled] = useState(style.image_enabled ?? false);
const [imageStyles, setImageStyles] = useState(
(style.image_style || 'flat-illustration').split(',').map(s => s.trim()).filter(Boolean)
);
const [imagePalette, setImagePalette] = useState(style.image_palette || 'auto');
const [imageCustomColors, setImageCustomColors] = useState(style.image_custom_colors || '');
const [imagePromptInstructions, setImagePromptInstructions] = useState(style.image_prompt_instructions || '');
// Подключение
const [botToken, setBotToken] = useState(channel.bot_token || '');
const [tgChannelId, setTgChannelId] = useState(channel.tg_channel_id || '');
const [tgUsername, setTgUsername] = useState(channel.tg_username || '');
const [vkToken, setVkToken] = useState(channel.vk_access_token || '');
const [tokenVerifying, setTokenVerifying] = useState(false);
const [tokenStatus, setTokenStatus] = useState(null); // null | 'ok' | 'error'
// AI-стиль
const [aiStylePrompt, setAiStylePrompt] = useState(channel.ai_style_prompt || '');
const [imageQuality, setImageQuality] = useState(channel.image_quality || 'standard');
// Авто-черновики
const [autoDraftEnabled, setAutoDraftEnabled] = useState(channel.auto_draft_enabled || false);
const [autoDraftCount, setAutoDraftCount] = useState(channel.auto_draft_count || 3);
const [autoDraftTime, setAutoDraftTime] = useState(channel.auto_draft_time || '08:00');
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
const [toast, setToast] = useState('');
async function save() {
setSaving(true);
setError('');
try {
const data = {
name, niche, audience, goal: goals.join(','), language,
bot_token: botToken.trim() || null,
tg_channel_id: tgChannelId.trim() || null,
tg_username: tgUsername.trim() || null,
vk_access_token: vkToken.trim() || null,
ai_style_prompt: aiStylePrompt.trim() || null,
image_quality: imageQuality,
auto_draft_enabled: autoDraftEnabled,
auto_draft_count: autoDraftCount,
auto_draft_time: autoDraftTime,
style: {
tone, formality, humor,
post_length: postLength,
emoji_level: emojiLevel,
hashtags_mode: hashtagsMode,
banned_words: bannedWords.split(',').map(s => s.trim()).filter(Boolean),
banned_topics: bannedTopics.split(',').map(s => s.trim()).filter(Boolean),
image_enabled: imageEnabled,
image_style: imageStyles.join(','),
image_palette: imagePalette,
image_custom_colors: imageCustomColors.trim() || null,
image_prompt_instructions: imagePromptInstructions.trim() || null,
},
};
const res = await fetch(`/api/channels/${channel.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(await res.text());
setToast('Сохранено');
setTimeout(() => setToast(''), 2500);
router.refresh();
} catch (e) { setError(e.message); }
finally { setSaving(false); }
}
async function del() {
if (!confirm(`Удалить канал «${channel.name}»? Это действие необратимо.`)) return;
setDeleting(true);
try {
await fetch(`/api/channels/${channel.id}`, { method: 'DELETE' });
router.push('/');
} catch (e) { setError(e.message); setDeleting(false); }
}
return (
<main className="max-w-3xl mx-auto p-4 sm:p-6">
<Link href={`/channels/${channel.id}`} className="btn-ghost mb-4 -ml-2">
<ArrowLeft className="w-4 h-4" /> К каналу
</Link>
<div className="flex items-center justify-between mb-6 flex-wrap gap-3">
<h1 className="text-2xl font-bold">Настройки канала</h1>
<div className="flex items-center gap-2">
{toast && <span className="text-sm text-accent">{toast}</span>}
<button onClick={del} disabled={deleting} className="btn-ghost text-sm text-red-400 hover:bg-red-500/10">
<Trash2 className="w-4 h-4" /> {deleting ? '...' : 'Удалить'}
</button>
<button onClick={save} disabled={saving} className="btn-primary">
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
{saving ? 'Сохраняю...' : 'Сохранить'}
</button>
</div>
</div>
{error && (
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2 mb-4">{error}</div>
)}
{/* Tabs */}
<div className="flex gap-1 border-b border-border mb-5">
{TABS.map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setTab(id)}
className={`px-4 py-2 text-sm font-medium border-b-2 inline-flex items-center gap-1.5 transition-colors ${
tab === id ? 'border-accent text-accent' : 'border-transparent text-gray-500 hover:text-gray-300'
}`}
>
<Icon className="w-4 h-4" />
{label}
</button>
))}
</div>
{/* TAB: Контент */}
{tab === 'content' && (
<div className="space-y-5">
<div className="card p-5 space-y-4">
<div>
<label className="label">Название</label>
<input className="input" value={name} onChange={e => setName(e.target.value)} />
</div>
<div>
<label className="label">Ниша</label>
<textarea className="input min-h-[70px]" value={niche} onChange={e => setNiche(e.target.value)} />
</div>
<div>
<label className="label">Аудитория</label>
<textarea className="input min-h-[70px]" value={audience} onChange={e => setAudience(e.target.value)} />
</div>
<div>
<label className="label">Цель канала <span className="text-gray-500 font-normal">(можно несколько)</span></label>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 mb-2">
{GOALS.map(g => {
const on = goals.includes(g.v);
return (
<button key={g.v} type="button"
onClick={() => setGoals(on ? goals.filter(x => x !== g.v) : [...goals, g.v])}
className={`p-2.5 rounded-lg border text-left transition-colors ${on ? 'border-accent bg-accent/10' : 'border-border bg-surface2 hover:border-gray-600'}`}>
<div className="text-sm font-medium">{g.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{g.desc}</div>
</button>
);
})}
</div>
<div className="flex gap-2">
<input className="input text-sm flex-1" placeholder="Своя цель — введи и нажми +"
value={customGoal} onChange={e => setCustomGoal(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); const v = customGoal.trim(); if (v && !goals.includes(v)) setGoals([...goals, v]); setCustomGoal(''); }}} />
<button type="button" onClick={() => { const v = customGoal.trim(); if (v && !goals.includes(v)) setGoals([...goals, v]); setCustomGoal(''); }}
disabled={!customGoal.trim()} className="btn-primary px-3 disabled:opacity-40"><Plus className="w-4 h-4" /></button>
</div>
{goals.filter(g => !GOALS.find(x => x.v === g)).length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{goals.filter(g => !GOALS.find(x => x.v === g)).map(g => (
<span key={g} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent/15 border border-accent/40 text-xs">
{g}<button type="button" onClick={() => setGoals(goals.filter(x => x !== g))}><X className="w-3 h-3" /></button>
</span>
))}
</div>
)}
</div>
<div>
<label className="label">Язык постов</label>
<select className="input" value={language} onChange={e => setLanguage(e.target.value)}>
<option value="ru">Русский</option>
<option value="en">English</option>
<option value="uk">Українська</option>
<option value="kk">Қазақша</option>
</select>
</div>
</div>
<div className="card p-5 space-y-4">
<h3 className="font-semibold text-sm">Стиль текста</h3>
<div>
<label className="label">Тон</label>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
{TONES.map(t => (
<button key={t.v} type="button" onClick={() => setTone(t.v)}
className={`px-3 py-2 rounded-lg border text-sm ${tone === t.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2'}`}>
{t.label}
</button>
))}
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="label">Обращение</label>
<div className="grid grid-cols-2 gap-2">
{[{ v: 'informal', l: 'На «ты»' }, { v: 'formal', l: 'На «вы»' }].map(o => (
<button key={o.v} type="button" onClick={() => setFormality(o.v)}
className={`px-3 py-2 rounded-lg border text-sm ${formality === o.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2'}`}>
{o.l}
</button>
))}
</div>
</div>
<div>
<label className="label">Юмор</label>
<select className="input" value={humor} onChange={e => setHumor(e.target.value)}>
{HUMOR.map(o => <option key={o.v} value={o.v}>{o.label}</option>)}
</select>
</div>
</div>
<div>
<label className="label">Длина постов</label>
<div className="grid grid-cols-3 gap-2">
{LENGTHS.map(l => (
<button key={l.v} type="button" onClick={() => setPostLength(l.v)}
className={`p-2.5 rounded-lg border text-left ${postLength === l.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2'}`}>
<div className="text-sm font-medium">{l.label}</div>
<div className="text-xs text-gray-500">{l.desc}</div>
</button>
))}
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="label">Эмодзи</label>
<select className="input" value={emojiLevel} onChange={e => setEmojiLevel(e.target.value)}>
{EMOJI.map(o => <option key={o.v} value={o.v}>{o.label}</option>)}
</select>
</div>
<div>
<label className="label">Хэштеги</label>
<select className="input" value={hashtagsMode} onChange={e => setHashtagsMode(e.target.value)}>
<option value="none">Не использовать</option>
<option value="end">В конце поста</option>
<option value="inline">Внутри текста</option>
</select>
</div>
</div>
<div>
<label className="label">Стоп-слова</label>
<input className="input" value={bannedWords} onChange={e => setBannedWords(e.target.value)}
placeholder="революционный, уникальный" />
</div>
<div>
<label className="label">Запрещённые темы</label>
<input className="input" value={bannedTopics} onChange={e => setBannedTopics(e.target.value)}
placeholder="политика, крипта" />
</div>
</div>
</div>
)}
{/* TAB: Картинки */}
{tab === 'images' && (
<div className="space-y-5">
<div className="card p-5">
<div className="flex items-center gap-3 mb-2">
<input
type="checkbox"
id="imageEnabled"
checked={imageEnabled}
onChange={e => setImageEnabled(e.target.checked)}
className="w-4 h-4 accent-accent"
/>
<label htmlFor="imageEnabled" className="font-semibold">Генерировать картинки к постам</label>
</div>
<p className="text-xs text-gray-500 ml-7">
Если включено в редакторе поста появится кнопка генерации картинки. Можешь генерировать вручную для каждого поста.
</p>
</div>
{imageEnabled && (
<>
<div className="card p-5">
<h3 className="font-semibold text-sm mb-1 flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-accent" />
Стиль изображений
<span className="text-gray-500 font-normal">(можно несколько система будет чередовать)</span>
</h3>
<p className="text-xs text-gray-500 mb-3">
Все стили это <b>AI-генерация</b>, не стоковые фото.
Если в посте упоминается реальный человек система автоматически ищет его фото в интернете вместо генерации.
</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{IMAGE_STYLES.map(s => {
const on = imageStyles.includes(s.v);
return (
<button
key={s.v} type="button"
onClick={() => setImageStyles(on ? imageStyles.filter(x => x !== s.v) : [...imageStyles, s.v])}
className={`p-3 rounded-lg border text-left transition-colors ${
on ? 'border-accent bg-accent/10' : 'border-border bg-surface2 hover:border-gray-600'
}`}
>
<div className="text-sm font-medium">{s.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{s.desc}</div>
</button>
);
})}
</div>
</div>
<div className="card p-5">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Palette className="w-4 h-4 text-accent" />
Цветовая палитра
</h3>
<div className="grid grid-cols-3 sm:grid-cols-7 gap-2 mb-4">
{IMAGE_PALETTES.map(p => (
<button
key={p.v} type="button" onClick={() => setImagePalette(p.v)}
className={`px-3 py-2 rounded-lg border text-sm ${
imagePalette === p.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2'
}`}
>
{p.label}
</button>
))}
</div>
<div>
<label className="label">Или брендовые цвета (опц.)</label>
<input
className="input font-mono text-sm"
value={imageCustomColors}
onChange={e => setImageCustomColors(e.target.value)}
placeholder="#10b981, #1e293b, #f59e0b"
/>
<div className="hint">Через запятую. Если заполнено приоритет над палитрой</div>
</div>
</div>
{/* Инструкции для AI */}
<div className="card p-5">
<h3 className="font-semibold text-sm mb-1 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-accent" />
Инструкции для AI
</h3>
<p className="text-xs text-gray-500 mb-3">
Опиши, какими должны быть картинки. Например: <em>«тёмный фон, минималистичные 3D-объекты, технологичная эстетика, без людей»</em>.
Применяется ко всем постам и обложкам статей этого канала.
</p>
<textarea
className="input min-h-[90px] text-sm"
value={imagePromptInstructions}
onChange={e => setImagePromptInstructions(e.target.value)}
placeholder="Примеры:&#10;— технологичный объект на тёмном градиентном фоне, как Stripe/Vercel blog&#10;— реалистичное фото молочного производства, без людей, фокус на деталях&#10;— мягкая пастельная акварель, природа и животные, тёплый тон"
maxLength={500}
/>
<div className="text-xs text-gray-500 text-right mt-1">{imagePromptInstructions.length}/500</div>
</div>
{/* Preview подсказка */}
<div className="card p-4 bg-accent/5 border-accent/20 text-sm text-gray-300">
<div className="font-medium text-accent mb-1">Как это работает</div>
<p className="text-xs">
AI прочитает контент поста и сгенерирует к нему обложку в выбранном стиле.
Генерация занимает ~30 секунд. Если что-то не нравится можно перегенерировать.
</p>
</div>
</>
)}
</div>
)}
{/* TAB: AI-стиль */}
{tab === 'ai' && (
<div className="space-y-5">
{/* Промт для генерации статей */}
<div className="card p-5 space-y-4">
<div className="flex items-center gap-2 mb-1">
<Sparkles className="w-4 h-4 text-accent" />
<h3 className="font-semibold text-sm">Стиль генерации статей</h3>
</div>
<p className="text-xs text-gray-400">
Дополнительные инструкции для AI при автоматической генерации статей в этом канале.
Применяется ко всем статьям канала. При ручной генерации можно переопределить.
</p>
<textarea
rows={5}
placeholder={`Например:\n• Пиши в стиле новостной заметки, без воды\n• Аудитория: молочные фермеры Сибири\n• Всегда заканчивай призывом к действию\n• Включай конкретные цифры и факты`}
value={aiStylePrompt}
onChange={e => setAiStylePrompt(e.target.value)}
className="input w-full text-sm resize-none"
/>
<p className="text-xs text-gray-500">
{aiStylePrompt.length}/1000 символов
</p>
</div>
{/* Модель генерации картинок — только gpt-5-image-mini */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-3">
<ImageIcon className="w-4 h-4 text-accent" />
<h3 className="font-semibold text-sm">Генерация картинок</h3>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-accent/5 border border-accent/20">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center text-base">🖼</div>
<div className="flex-1">
<div className="text-sm font-medium">gpt-5-image-mini</div>
<div className="text-xs text-gray-400 mt-0.5">routerai.ru ~2.72/картинка high quality</div>
</div>
<span className="text-xs px-2 py-0.5 rounded bg-green-500/20 text-green-400">Активна</span>
</div>
<p className="text-xs text-gray-500 mt-2">
Единственная модель для генерации изображений. Параметр качества фиксирован провайдером.
</p>
</div>
{/* Банк тем */}
<TopicBank channelId={channel.id} />
{/* Авто-черновики */}
<div className="card p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-sm flex items-center gap-2">
<span></span> Авто-генерация черновиков
</h3>
<p className="text-xs text-gray-400 mt-0.5">
Система генерирует посты каждый день ты одобряешь вечером
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer"
checked={autoDraftEnabled}
onChange={e => setAutoDraftEnabled(e.target.checked)} />
<div className="w-10 h-5 bg-gray-600 peer-focus:outline-none rounded-full peer
peer-checked:after:translate-x-full peer-checked:after:border-white
after:content-[''] after:absolute after:top-0.5 after:left-[2px]
after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all
peer-checked:bg-accent" />
</label>
</div>
{autoDraftEnabled && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label text-xs mb-1">Постов в день</label>
<select value={autoDraftCount} onChange={e => setAutoDraftCount(+e.target.value)}
className="input w-full text-sm py-1.5">
{[1,2,3,5,7,10].map(n => <option key={n} value={n}>{n} {n===1?'пост':'постов'}</option>)}
</select>
</div>
<div>
<label className="label text-xs mb-1">Время генерации</label>
<input type="time" value={autoDraftTime}
onChange={e => setAutoDraftTime(e.target.value)}
className="input w-full text-sm py-1.5" />
</div>
</div>
)}
<p className="text-xs text-gray-500">
Черновики появляются на странице{' '}
<a href="/drafts" target="_blank" className="text-accent hover:underline">Черновики</a>.
Там можно редактировать, одобрять и планировать публикацию.
</p>
</div>
</div>
)}
{/* TAB: Подключение */}
{tab === 'connect' && (
<div className="space-y-5">
<div className="card p-5 space-y-4">
<div className="flex items-center gap-2 mb-1">
<span className="text-lg"></span>
<h3 className="font-semibold">Telegram</h3>
</div>
<div className="bg-surface2/50 rounded-lg p-3 text-xs text-gray-400 space-y-1 border border-border">
<div className="font-medium text-gray-300 mb-2">Как подключить:</div>
<div>1. Создай бота через <span className="text-accent">@BotFather</span> <code>/newbot</code> скопируй токен</div>
<div>2. Добавь бота <b>администратором</b> в свой канал (права: публикация сообщений)</div>
<div>3. Узнай ID канала: перешли сообщение из канала боту <span className="text-accent">@idbot</span> вернёт ID вида <code>-100xxxxxxxxxx</code></div>
</div>
<div>
<label className="label">Bot Token</label>
<input className="input font-mono text-sm" placeholder="7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
value={botToken} onChange={e => { setBotToken(e.target.value); setTokenStatus(null); }} type="password" />
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="label">ID канала</label>
<input className="input font-mono text-sm" placeholder="-1001234567890"
value={tgChannelId} onChange={e => setTgChannelId(e.target.value)} />
<div className="hint">Начинается с -100</div>
</div>
<div>
<label className="label">Username канала</label>
<input className="input font-mono text-sm" placeholder="@mychannel"
value={tgUsername} onChange={e => setTgUsername(e.target.value)} />
<div className="hint">Необязательно если заполнен ID</div>
</div>
</div>
</div>
<div className="card p-5 space-y-4">
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">🅱</span>
<h3 className="font-semibold">ВКонтакте</h3>
</div>
<div>
<label className="label">Access Token группы</label>
<input className="input font-mono text-sm" placeholder="vk1.a.xxx..."
value={vkToken} onChange={e => setVkToken(e.target.value)} type="password" />
<div className="hint">Управление API Ключи доступа Создать ключ (права: wall, photos)</div>
</div>
</div>
</div>
)}
</main>
);
}