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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user