forked from admin/zeropost-tool
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:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user