feat: custom prompt UI + AI-style tab in ChannelEdit

ChannelEdit:
- Вкладка «AI-стиль»: textarea для ai_style_prompt, выбор image_quality (standard/hd)
- Описание моделей: gpt-5-image-mini vs gpt-5.4-image-2 с ценами в кредитах

ChannelView:
- Коллапсируемое поле «Доп. инструкции для AI» под темой поста
- Индикатор (синяя точка) если промт заполнен
- customPrompt передаётся в /api/generate
This commit is contained in:
Ник (Claude)
2026-06-11 15:15:22 +03:00
parent f4860f0e70
commit 8d015add30
2 changed files with 119 additions and 1 deletions
+85
View File
@@ -63,6 +63,7 @@ const IMAGE_PALETTES = [
const TABS = [
{ id: 'content', label: 'Контент', icon: Type },
{ id: 'images', label: 'Картинки', icon: ImageIcon },
{ id: 'ai', label: 'AI-стиль', icon: Sparkles },
{ id: 'connect', label: 'Подключение', icon: Plug },
];
@@ -106,6 +107,10 @@ export default function ChannelEdit({ channel }) {
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 [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
@@ -121,6 +126,8 @@ export default function ChannelEdit({ channel }) {
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,
style: {
tone, formality, humor,
post_length: postLength,
@@ -442,6 +449,84 @@ export default function ChannelEdit({ channel }) {
</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>
{/* Качество изображений */}
<div className="card p-5 space-y-4">
<div className="flex items-center gap-2 mb-1">
<ImageIcon className="w-4 h-4 text-accent" />
<h3 className="font-semibold text-sm">Качество генерации картинок</h3>
</div>
<div className="grid grid-cols-2 gap-3">
{[
{
v: 'standard',
label: 'Стандарт',
model: 'gpt-5-image-mini',
desc: 'Быстро, дешевле. Подходит для большинства постов в TG и ВК.',
cost: '3 кредита / картинка',
badge: 'Рекомендуется',
},
{
v: 'hd',
label: 'HD',
model: 'gpt-5.4-image-2',
desc: 'Лучшее качество, фотореализм, поддержка текста на картинке.',
cost: '10 кредитов / картинка',
badge: 'Для текста на фото',
},
].map(opt => (
<button
key={opt.v}
type="button"
onClick={() => setImageQuality(opt.v)}
className={`p-4 rounded-xl border text-left transition-all ${
imageQuality === opt.v
? 'border-accent bg-accent/10'
: 'border-border hover:border-accent/40'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-sm">{opt.label}</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${
opt.v === 'standard' ? 'bg-green-500/20 text-green-400' : 'bg-blue-500/20 text-blue-400'
}`}>{opt.badge}</span>
</div>
<div className="text-xs text-gray-400 mb-2">{opt.model}</div>
<div className="text-xs text-gray-300">{opt.desc}</div>
<div className={`text-xs mt-2 font-medium ${
opt.v === 'standard' ? 'text-green-400' : 'text-blue-400'
}`}>{opt.cost}</div>
</button>
))}
</div>
</div>
</div>
)}
{/* TAB: Подключение */}
{tab === 'connect' && (
<div className="space-y-5">
+34 -1
View File
@@ -38,6 +38,8 @@ function stripCaption(text) {
export default function ChannelView({ channel }) {
const [topic, setTopic] = useState('');
const [customPrompt, setCustomPrompt] = useState('');
const [showCustomPrompt, setShowCustomPrompt] = useState(false);
const [generating, setGenerating] = useState(false);
const [post, setPost] = useState(null);
const [error, setError] = useState('');
@@ -225,6 +227,7 @@ export default function ChannelView({ channel }) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'post', channelId: channel.id, topic: useTopic, useCritique: true,
customPrompt: customPrompt.trim() || undefined,
}),
});
const job = await createRes.json();
@@ -407,12 +410,42 @@ export default function ChannelView({ channel }) {
)}
<textarea
className="input min-h-[80px] mb-3"
className="input min-h-[80px] mb-2"
value={topic}
onChange={e => setTopic(e.target.value)}
placeholder="Тема поста — конкретный заход, не общая категория. Например: «OpenAI выпустил Memory — что это даёт маркетологу»"
disabled={generating}
/>
{/* Дополнительные инструкции */}
<div className="mb-3">
<button
type="button"
onClick={() => setShowCustomPrompt(v => !v)}
className="text-xs text-gray-500 hover:text-accent flex items-center gap-1 transition-colors"
>
<span>{showCustomPrompt ? '▾' : '▸'}</span>
Дополнительные инструкции для AI
{customPrompt.trim() && <span className="ml-1 w-1.5 h-1.5 rounded-full bg-accent inline-block" />}
</button>
{showCustomPrompt && (
<div className="mt-2 space-y-1">
<textarea
rows={3}
className="input w-full text-sm resize-none"
placeholder={`Например: «Сделай акцент на кейсах из сельского хозяйства» или «Добавь призыв подписаться в конце»`}
value={customPrompt}
onChange={e => setCustomPrompt(e.target.value)}
disabled={generating}
/>
<p className="text-xs text-gray-500">
Перебивает стиль канала для этой генерации.
{channel.ai_style_prompt && ' Канальный промт также будет применён.'}
</p>
</div>
)}
</div>
<div className="flex items-center justify-between gap-3">
<div className="text-xs text-gray-500">
ИИ напишет пост в стиле твоего канала с учётом примеров