feat: multi-select image styles, fix descriptions

- IMAGE_STYLES: исправлены описания (realistic-photo = AI-фотореализм, не сток)
- Стиль изображений: single-select → multi-select (чередуется случайно)
- Добавлено пояснение: AI-генерация ≠ стоковые фото; реальный человек → поиск фото
- DB: channel_style.image_style varchar(30) → varchar(255)
This commit is contained in:
Ник (Claude)
2026-06-10 15:50:49 +03:00
parent e330ac3871
commit 8244789f10
+20 -9
View File
@@ -40,12 +40,12 @@ const EMOJI = [
]; ];
const IMAGE_STYLES = [ const IMAGE_STYLES = [
{ v: 'realistic-photo', label: 'Реалистичное фото', desc: 'Стоковая фотография' }, { v: 'realistic-photo', label: 'Реалистичное фото', desc: 'AI-фотореализм, не сток' },
{ v: 'flat-illustration',label: 'Плоская иллюстрация', desc: 'Editorial vector' }, { v: 'flat-illustration',label: 'Плоская иллюстрация', desc: 'Editorial vector' },
{ v: '3d-render', label: '3D рендер', desc: 'Pixar-like' }, { v: '3d-render', label: '3D рендер', desc: 'Pixar-like' },
{ v: 'cartoon', label: 'Мультяшный', desc: 'Comic book' }, { v: 'cartoon', label: 'Мультяшный', desc: 'Comic book' },
{ v: 'minimal', label: 'Минимализм', desc: 'Один элемент' }, { v: 'minimal', label: 'Минимализм', desc: 'Один элемент' },
{ v: 'abstract', label: 'Абстракция', desc: 'Без объектов' }, { v: 'abstract', label: 'Абстракция', desc: 'Геометрия, настроение' },
{ v: 'sketch', label: 'Скетч', desc: 'Карандашный рисунок' }, { v: 'sketch', label: 'Скетч', desc: 'Карандашный рисунок' },
{ v: 'cyberpunk', label: 'Киберпанк', desc: 'Неон, будущее' }, { v: 'cyberpunk', label: 'Киберпанк', desc: 'Неон, будущее' },
]; ];
@@ -91,7 +91,9 @@ export default function ChannelEdit({ channel }) {
// Картинки // Картинки
const [imageEnabled, setImageEnabled] = useState(style.image_enabled ?? false); const [imageEnabled, setImageEnabled] = useState(style.image_enabled ?? false);
const [imageStyle, setImageStyle] = useState(style.image_style || 'flat-illustration'); 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 [imagePalette, setImagePalette] = useState(style.image_palette || 'auto');
const [imageCustomColors, setImageCustomColors] = useState(style.image_custom_colors || ''); const [imageCustomColors, setImageCustomColors] = useState(style.image_custom_colors || '');
const [imagePromptInstructions, setImagePromptInstructions] = useState(style.image_prompt_instructions || ''); const [imagePromptInstructions, setImagePromptInstructions] = useState(style.image_prompt_instructions || '');
@@ -127,7 +129,7 @@ export default function ChannelEdit({ channel }) {
banned_words: bannedWords.split(',').map(s => s.trim()).filter(Boolean), banned_words: bannedWords.split(',').map(s => s.trim()).filter(Boolean),
banned_topics: bannedTopics.split(',').map(s => s.trim()).filter(Boolean), banned_topics: bannedTopics.split(',').map(s => s.trim()).filter(Boolean),
image_enabled: imageEnabled, image_enabled: imageEnabled,
image_style: imageStyle, image_style: imageStyles.join(','),
image_palette: imagePalette, image_palette: imagePalette,
image_custom_colors: imageCustomColors.trim() || null, image_custom_colors: imageCustomColors.trim() || null,
image_prompt_instructions: imagePromptInstructions.trim() || null, image_prompt_instructions: imagePromptInstructions.trim() || null,
@@ -350,22 +352,31 @@ export default function ChannelEdit({ channel }) {
{imageEnabled && ( {imageEnabled && (
<> <>
<div className="card p-5"> <div className="card p-5">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2"> <h3 className="font-semibold text-sm mb-1 flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-accent" /> <ImageIcon className="w-4 h-4 text-accent" />
Стиль изображений Стиль изображений
<span className="text-gray-500 font-normal">(можно несколько система будет чередовать)</span>
</h3> </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"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{IMAGE_STYLES.map(s => ( {IMAGE_STYLES.map(s => {
const on = imageStyles.includes(s.v);
return (
<button <button
key={s.v} type="button" onClick={() => setImageStyle(s.v)} 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 ${ className={`p-3 rounded-lg border text-left transition-colors ${
imageStyle === s.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2 hover:border-gray-600' 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-sm font-medium">{s.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{s.desc}</div> <div className="text-xs text-gray-500 mt-0.5">{s.desc}</div>
</button> </button>
))} );
})}
</div> </div>
</div> </div>