Update the Ideogram 4 prompt generation/parsing/ui to handle the updated format notes better.

This commit is contained in:
Jaret Burkett
2026-06-16 09:44:38 -06:00
parent fcccc0fbd2
commit 86b19589a0
10 changed files with 497 additions and 155 deletions

View File

@@ -18,10 +18,47 @@ export function isIdeogramCaption(text: string): boolean {
}
}
// Normalize an arbitrary color string to a #rrggbb value usable by <input type=color>.
// Official medium tokens. `photograph` uses the `photo` style key; every other
// medium uses `art_style`. Users may also type a custom medium.
const MEDIUM_OPTIONS = ['photograph', 'illustration', '3d_render', 'painting', 'graphic_design'];
// Map old/variant medium spellings (e.g. our old "Illustration." / "3D render.")
// to a canonical token so the dropdown can match them; unknown values stay custom.
function canonMedium(m: string): string {
const key = (m || '').trim().replace(/\.+$/, '').trim().toLowerCase();
const aliases: Record<string, string> = {
photograph: 'photograph',
photo: 'photograph',
illustration: 'illustration',
'3d render': '3d_render',
'3d_render': '3d_render',
'3d-render': '3d_render',
'3drender': '3d_render',
render: '3d_render',
'3d': '3d_render',
painting: 'painting',
'graphic design': 'graphic_design',
graphic_design: 'graphic_design',
'graphic-design': 'graphic_design',
graphic: 'graphic_design',
};
return aliases[key] ?? (m || '').trim();
}
// Photograph branch uses `photo`; everything else uses `art_style`. An unknown
// (empty/custom) medium defaults to the photo branch.
function isPhotoMedium(m: string): boolean {
const c = canonMedium(m);
if (c === 'photograph') return true;
if (MEDIUM_OPTIONS.includes(c)) return false;
return true;
}
// Normalize a color to an UPPERCASE #RRGGBB value (expands #RGB). Returns
// '#000000' for anything unparseable (used to feed <input type=color>).
function toHex6(c: string): string {
const s = (c || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s;
if (/^#[0-9a-fA-F]{6}$/.test(s)) return '#' + s.slice(1).toUpperCase();
if (/^#[0-9a-fA-F]{3}$/.test(s)) {
return (
'#' +
@@ -30,6 +67,7 @@ function toHex6(c: string): string {
.split('')
.map(ch => ch + ch)
.join('')
.toUpperCase()
);
}
return '#000000';
@@ -99,6 +137,49 @@ function TextAreaField({
);
}
// Medium picker: a dropdown of the official tokens plus a "Custom…" escape hatch
// that reveals a free-text input. Recognizes old/variant spellings via canonMedium.
function MediumField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const canon = canonMedium(value);
const known = MEDIUM_OPTIONS.includes(canon);
const [custom, setCustom] = useState(!known && (value || '').trim() !== '');
const showCustom = custom || (!known && (value || '').trim() !== '');
return (
<label className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400">Medium</span>
<select
value={showCustom ? '__custom__' : canon}
onChange={e => {
const v = e.target.value;
if (v === '__custom__') setCustom(true);
else {
setCustom(false);
onChange(v);
}
}}
className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-100 outline-none focus:border-blue-500"
>
{MEDIUM_OPTIONS.map(m => (
<option key={m} value={m}>
{m}
</option>
))}
<option value="__custom__">Custom…</option>
</select>
{showCustom && (
<input
type="text"
value={value}
placeholder="custom medium"
spellCheck={false}
onChange={e => onChange(e.target.value)}
className="mt-1 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-100 outline-none focus:border-blue-500"
/>
)}
</label>
);
}
function ColorPalette({ colors, max, onChange }: { colors: string[]; max: number; onChange: (c: string[]) => void }) {
const setAt = (i: number, v: string) => onChange(colors.map((c, idx) => (idx === i ? v : c)));
const removeAt = (i: number) => onChange(colors.filter((_, idx) => idx !== i));
@@ -109,7 +190,7 @@ function ColorPalette({ colors, max, onChange }: { colors: string[]; max: number
<input
type="color"
value={toHex6(c)}
onChange={e => setAt(i, e.target.value)}
onChange={e => setAt(i, toHex6(e.target.value))}
className="w-5 h-5 rounded cursor-pointer bg-transparent border-0 p-0"
title="Pick color"
/>
@@ -218,6 +299,40 @@ export default function IdeogramCaptionSidebar({
d.style_description = sd;
});
// Setting the medium may flip the branch (photograph ↔ non-photo). When it does,
// migrate the render text from photo↔art_style so we never keep both keys.
const setMedium = (value: string) =>
update(d => {
const sd = { ...(d.style_description || {}) };
const wasPhoto = isPhotoMedium(sd.medium ?? '');
const nowPhoto = isPhotoMedium(value);
sd.medium = value;
if (wasPhoto !== nowPhoto) {
const val = sd.photo ?? sd.art_style ?? '';
delete sd.photo;
delete sd.art_style;
if (nowPhoto) sd.photo = val;
else sd.art_style = val;
}
d.style_description = sd;
});
// The single render field writes to `photo` or `art_style` per the current branch.
const photoBranch = isPhotoMedium(style.medium ?? '');
const renderValue = (photoBranch ? style.photo : style.art_style) ?? style.photo ?? style.art_style ?? '';
const setRender = (value: string) =>
update(d => {
const sd = { ...(d.style_description || {}) };
if (isPhotoMedium(sd.medium ?? '')) {
sd.photo = value;
delete sd.art_style;
} else {
sd.art_style = value;
delete sd.photo;
}
d.style_description = sd;
});
const setElement = (i: number, mutator: (el: any) => void) =>
update(d => {
const els = d?.compositional_deconstruction?.elements;
@@ -279,12 +394,12 @@ export default function IdeogramCaptionSidebar({
<Section title="Style">
<TextField label="Aesthetics" value={style.aesthetics ?? ''} onChange={v => setStyle('aesthetics', v)} />
<TextField label="Lighting" value={style.lighting ?? ''} onChange={v => setStyle('lighting', v)} />
<TextField label="Photo / render" value={style.photo ?? ''} onChange={v => setStyle('photo', v)} />
<MediumField value={style.medium ?? ''} onChange={setMedium} />
<TextField
label="Medium"
value={style.medium ?? ''}
onChange={v => setStyle('medium', v)}
placeholder="Photograph."
label={photoBranch ? 'Photo (camera / film)' : 'Art style (rendering technique)'}
value={renderValue}
onChange={setRender}
placeholder={photoBranch ? '35mm film still, shallow depth of field' : 'flat vector, clean edges'}
/>
<div className="flex flex-col gap-1">
<span className="text-[10px] text-gray-400">Color palette (max 16)</span>