Draw: bare bones enhancer

This commit is contained in:
Enrico Ros
2024-06-12 03:31:09 -07:00
parent 364ad63877
commit 02c9f3ebdb
5 changed files with 167 additions and 143 deletions
+26 -26
View File
@@ -213,33 +213,33 @@ export function DrawCreate(props: {
}}
>
{/* Draw history (last 50) */}
{/*<Box sx={{*/}
{/* // my: 'auto',*/}
{/* // display: 'flex', flexDirection: 'column', alignItems: 'center',*/}
{/* border: DEBUG_LAYOUT ? '1px solid purple' : undefined,*/}
{/* minHeight: '300px',*/}
{/* // layout*/}
{/* display: 'grid',*/}
{/* gridTemplateColumns: props.isMobile*/}
{/* ? 'repeat(auto-fit, minmax(320px, 1fr))'*/}
{/* : 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',*/}
{/* gap: { xs: 2, md: 2 },*/}
{/*}}>*/}
{/* <Box sx={{*/}
{/* // my: 'auto',*/}
{/* // display: 'flex', flexDirection: 'column', alignItems: 'center',*/}
{/* border: DEBUG_LAYOUT ? '1px solid purple' : undefined,*/}
{/* minHeight: '300px',*/}
{/* // layout*/}
{/* display: 'grid',*/}
{/* gridTemplateColumns: props.isMobile*/}
{/* ? 'repeat(auto-fit, minmax(320px, 1fr))'*/}
{/* : 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',*/}
{/* gap: { xs: 2, md: 2 },*/}
{/* }}>*/}
{/* {prompts.map((prompt, _index) => {*/}
{/* return (*/}
{/* <TempPromptImageGen*/}
{/* key={prompt.uuid}*/}
{/* prompt={prompt}*/}
{/* sx={{*/}
{/* border: DEBUG_LAYOUT ? '1px solid green' : undefined,*/}
{/* }}*/}
{/* />*/}
{/* );*/}
{/* })}*/}
{/* </Box>*/}
{/* {prompts.map((prompt, _index) => {*/}
{/* return (*/}
{/* <TempPromptImageGen*/}
{/* key={prompt.uuid}*/}
{/* prompt={prompt}*/}
{/* sx={{*/}
{/* border: DEBUG_LAYOUT ? '1px solid green' : undefined,*/}
{/* }}*/}
{/* />*/}
{/* );*/}
{/* })}*/}
{/*</Box>*/}
{/* Fallbac*/}
<FallbackNoImages />
+14 -12
View File
@@ -28,24 +28,26 @@ export function ButtonPromptFromIdea(props: {
return props.isMobile ? null : (
<ButtonGroup
variant='soft' color='neutral'
variant='outlined' color='neutral'
disabled={props.disabled}
sx={{
// '--ButtonGroup-separatorSize': 0,
minWidth: 160,
}}
>
<Button
fullWidth onClick={handleIdeaNext}
startDecorator={<LightbulbOutlinedIcon />}
sx={{
// '--Button-gap': 'auto',
// minWidth: 100,
justifyContent: 'flex-start',
transition: 'background-color 0.2s, color 0.2s',
}}>
Idea
</Button>
<Tooltip disableInteractive title='New Idea'>
<Button
fullWidth onClick={handleIdeaNext}
startDecorator={<LightbulbOutlinedIcon />}
sx={{
// '--Button-gap': 'auto',
// minWidth: 100,
justifyContent: 'flex-start',
transition: 'background-color 0.2s, color 0.2s',
}}>
Idea
</Button>
</Tooltip>
<Tooltip disableInteractive title='Use Idea'>
<IconButton size='sm' onClick={onIdeaUse}>
<ArrowForwardRoundedIcon />
+105 -96
View File
@@ -14,12 +14,13 @@ import NumbersRoundedIcon from '@mui/icons-material/NumbersRounded';
import RemoveIcon from '@mui/icons-material/Remove';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
import { animationEnterBelow } from '~/common/util/animUtils';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { ButtonPromptFromIdea } from './ButtonPromptFromIdea';
import { ButtonPromptFromX } from './ButtonPromptFromX';
import { useDrawIdeas } from './useDrawIdeas';
@@ -51,10 +52,11 @@ export function PromptComposer(props: {
const [nextPrompt, setNextPrompt] = React.useState<string>('');
const [tempCount, setTempCount] = React.useState<number>(1);
const [tempRepeat, setTempRepeat] = React.useState<number>(1);
const [isSimpleEnhancing, setIsSimpleEnhancing] = React.useState<boolean>(false);
const [showMobileRepeat, setShowMobileRepeat] = React.useState<boolean>(false);
// external state
const { currentIdea, nextRandomIdea } = useDrawIdeas();
const { currentIdea, nextRandomIdea, clearCurrentIdea } = useDrawIdeas();
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
@@ -77,6 +79,7 @@ export function PromptComposer(props: {
const handlePromptEnqueue = React.useCallback(() => {
setNextPrompt('');
clearCurrentIdea();
if (nonEmptyPrompt?.trim()) {
onPromptEnqueue([{
uuid: uuidv4(),
@@ -84,7 +87,7 @@ export function PromptComposer(props: {
_repeatCount: isRepeatShown ? tempRepeat : 1,
}]);
}
}, [isRepeatShown, nonEmptyPrompt, onPromptEnqueue, tempRepeat]);
}, [clearCurrentIdea, isRepeatShown, nonEmptyPrompt, onPromptEnqueue, tempRepeat]);
// Type...
@@ -109,108 +112,114 @@ export function PromptComposer(props: {
// Ideas
const handleIdeaUse = React.useCallback(() => {
currentIdeaPrompt && setNextPrompt(currentIdeaPrompt);
}, [currentIdeaPrompt]);
// PromptFx
const handleSimpleEnhance = React.useCallback(async () => {
if (nonEmptyPrompt?.trim()) {
setIsSimpleEnhancing(true);
const improvedPrompt = await imaginePromptFromText(nonEmptyPrompt, null).catch(console.error);
if (improvedPrompt)
setNextPrompt(improvedPrompt);
setIsSimpleEnhancing(false);
}
}, [nonEmptyPrompt]);
const textEnrichComponents = React.useMemo(() => {
const textEnrichComponents = React.useMemo(() => (
<Box sx={{
flex: 1,
margin: 1,
marginTop: 0,
const handleClickMissing = (_event: React.MouseEvent) => {
alert('Not implemented yet');
};
// layout
display: 'flex', flexFlow: 'row wrap', alignItems: 'center', gap: 1,
return (
// PromptFx Buttons
<Box sx={{
flex: 1,
margin: 1,
// Buttons (tagged by class)
[`& .${promptButtonClass}`]: {
'--Button-gap': '1.2rem',
transition: 'background-color 0.2s, color 0.2s',
minWidth: 100,
},
}}>
// layout
display: 'flex', flexFlow: 'row wrap', alignItems: 'center', gap: 1,
{/* Change / Use idea */}
{/*{props.isMobile && (*/}
{/* <ButtonGroup variant='soft' color='neutral' sx={{ borderRadius: 'sm' }}>*/}
{/* <Button className={promptButtonClass} disabled={userHasText} onClick={handleIdeaNext}>*/}
{/* Idea*/}
{/* </Button>*/}
{/* <Tooltip disableInteractive title='Use Idea'>*/}
{/* <IconButton onClick={handleIdeaUse}>*/}
{/* <ArrowDownwardIcon />*/}
{/* </IconButton>*/}
{/* </Tooltip>*/}
{/* </ButtonGroup>*/}
{/*)}*/}
// Buttons (tagged by class)
[`& .${promptButtonClass}`]: {
'--Button-gap': '1.2rem',
transition: 'background-color 0.2s, color 0.2s',
minWidth: 100,
},
}}>
{/* PromptFx */}
<Button
variant={isSimpleEnhancing ? 'solid' : 'soft'}
color='primary'
disabled={!userHasText}
loading={isSimpleEnhancing}
className={promptButtonClass}
endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}
onClick={handleSimpleEnhance}
sx={{
boxShadow: (!userHasText || isSimpleEnhancing) ? undefined : '0 6px 6px -6px rgb(var(--joy-palette-primary-darkChannel) / 40%)',
borderRadius: 'xs',
// boxShadow: 'xs'
}}
>
Enhance
</Button>
{/* Change / Use idea */}
{/*{props.isMobile && (*/}
{/* <ButtonGroup variant='soft' color='neutral' sx={{ borderRadius: 'sm' }}>*/}
{/* <Button className={promptButtonClass} disabled={userHasText} onClick={handleIdeaNext}>*/}
{/* Idea*/}
{/* </Button>*/}
{/* <Tooltip disableInteractive title='Use Idea'>*/}
{/* <IconButton onClick={handleIdeaUse}>*/}
{/* <ArrowDownwardIcon />*/}
{/* </IconButton>*/}
{/* </Tooltip>*/}
{/* </ButtonGroup>*/}
{/*)}*/}
{/*<Button*/}
{/* variant='soft' color='success'*/}
{/* disabled={!userHasText}*/}
{/* className={promptButtonClass}*/}
{/* endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}*/}
{/* onClick={handleClickMissing}*/}
{/* sx={{ borderRadius: 'sm' }}*/}
{/*>*/}
{/* Restyle*/}
{/*</Button>*/}
{/* PromptFx */}
<Button
variant='soft' color='success'
disabled={!userHasText}
className={promptButtonClass}
endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}
onClick={handleClickMissing}
sx={{ borderRadius: 'sm' }}
>
Enhance
</Button>
{/*<Button*/}
{/* variant='soft' color='success'*/}
{/* disabled={!userHasText}*/}
{/* className={promptButtonClass}*/}
{/* endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}*/}
{/* onClick={handleClickMissing}*/}
{/* sx={{ borderRadius: 'sm' }}*/}
{/*>*/}
{/* Restyle*/}
{/*</Button>*/}
<ButtonGroup sx={{ ml: 'auto' }}>
{tempCount > 1 && <IconButton onClick={() => setTempCount(count => count - 1)}>
<RemoveIcon />
</IconButton>}
{tempCount > 1 && <>
<IconButton>
<KeyboardArrowLeftIcon />
</IconButton>
<Button
sx={{
px: 0,
minWidth: '3rem',
pointerEvents: 'none',
}}>
<Typography level='body-xs' color='danger' sx={{ fontWeight: 'lg' }}>
{tempCount > 1 ? `1 / ${tempCount}` : '1'}
</Typography>
</Button>
<IconButton>
<KeyboardArrowRightIcon />
</IconButton>
</>}
<IconButton onClick={() => setTempCount(count => count + 1)}>
<AddIcon />
<ButtonGroup sx={{ ml: 'auto' }}>
{tempCount > 1 && <IconButton onClick={() => setTempCount(count => count - 1)}>
<RemoveIcon />
</IconButton>}
{tempCount > 1 && <>
<IconButton>
<KeyboardArrowLeftIcon />
</IconButton>
</ButtonGroup>
<Button
sx={{
px: 0,
minWidth: '3rem',
pointerEvents: 'none',
}}>
<Typography level='body-xs' color='danger' sx={{ fontWeight: 'lg' }}>
{tempCount > 1 ? `1 / ${tempCount}` : '1'}
</Typography>
</Button>
<IconButton>
<KeyboardArrowRightIcon />
</IconButton>
</>}
<IconButton onClick={() => setTempCount(count => count + 1)}>
<AddIcon />
</IconButton>
</ButtonGroup>
{/* Char counter */}
{/*<Typography level='body-sm' sx={{ ml: 'auto', mr: 1 }}>*/}
{/* {!!nonEmptyPrompt?.length && nonEmptyPrompt.length.toLocaleString()}*/}
{/*</Typography>*/}
</Box>
);
}, [tempCount, userHasText]);
{/* Char counter */}
{/*<Typography level='body-sm' sx={{ ml: 'auto', mr: 1 }}>*/}
{/* {!!nonEmptyPrompt?.length && nonEmptyPrompt.length.toLocaleString()}*/}
{/*</Typography>*/}
</Box>
), [handleSimpleEnhance, isSimpleEnhancing, tempCount, userHasText]);
return (
<Box aria-label='Drawing Prompt' component='section' sx={props.sx}>
@@ -235,9 +244,9 @@ export function PromptComposer(props: {
<MenuItem>
<ButtonPromptFromIdea disabled={userHasText} onIdeaNext={nextRandomIdea} onIdeaUse={handleIdeaUse} />
</MenuItem>
<MenuItem>
<ButtonPromptFromX name='Image' disabled />
</MenuItem>
{/*<MenuItem>*/}
{/* <ButtonPromptFromX name='Image' disabled />*/}
{/*</MenuItem>*/}
{/*<MenuItem>*/}
{/* <ButtonPromptFromPlaceholder name='Chat' disabled />*/}
{/*</MenuItem>*/}
@@ -250,7 +259,7 @@ export function PromptComposer(props: {
<ButtonPromptFromIdea disabled={userHasText} onIdeaNext={nextRandomIdea} onIdeaUse={handleIdeaUse} />
<ButtonPromptFromX name='Image' disabled />
{/*<ButtonPromptFromX name='Image' disabled />*/}
{/*<ButtonPromptFromPlaceholder name='Chats' disabled />*/}
@@ -268,7 +277,7 @@ export function PromptComposer(props: {
value={nextPrompt}
onChange={handleTextareaTextChange}
onKeyDown={handleTextareaKeyDown}
startDecorator={textEnrichComponents}
endDecorator={textEnrichComponents}
slotProps={{
textarea: {
enterKeyHint: enterIsNewline ? 'enter' : 'send',
+5 -1
View File
@@ -52,5 +52,9 @@ export function useDrawIdeas() {
});
}, []);
return { allIdeas, currentIdea, nextRandomIdea };
const clearCurrentIdea = React.useCallback(() => {
setCurrentIdea(null);
}, []);
return { allIdeas, currentIdea, nextRandomIdea, clearCurrentIdea };
}
@@ -1,24 +1,33 @@
import { getFastLLMId } from '~/modules/llms/store-llms';
import { getChatLLMId } from '~/modules/llms/store-llms';
import { llmChatGenerateOrThrow, VChatMessageIn } from '~/modules/llms/llm.client';
const simpleImagineSystemPrompt =
`As an AI art prompt writer, create captivating prompts using adjectives, nouns, and artistic references that a non-technical person can understand.
`As an AI image generation prompt writer, create captivating but clear and simple prompts using adjectives, nouns, and artistic references that a non-technical person can understand.
Craft creative, coherent and descriptive captions to guide the AI in generating visually striking artwork.
Provide output as a lowercase prompt and nothing else.`;
Follow best practices such as beginning with 'A [photo of, drawing of, ...] {subject} ...', using objective words that are unambiguous to visualize.
Write a minimum of 20-30 words prompt and up to the size of the input.
Provide output a single image generation prompt and nothing else.`;
/**
* Creates a caption for a drawing or photo given some description - used to elevate the quality of the imaging
*/
export async function imaginePromptFromText(messageText: string, contextRef: string): Promise<string | null> {
const fastLLMId = getFastLLMId();
if (!fastLLMId) return null;
export async function imaginePromptFromText(messageText: string, contextRef: string | null): Promise<string | null> {
// we used the fast LLM, but let's just converge to the chat LLM here
const chatLLMId = getChatLLMId();
if (!chatLLMId) return null;
// truncate the messageText to full words and up to 1000 characters
const lastSpace = messageText.slice(0, 1000).lastIndexOf(' ');
messageText = messageText.slice(0, lastSpace > 0 ? lastSpace : 1000);
if (!/[.!?]$/.test(messageText)) messageText += '.';
try {
const instructions: VChatMessageIn[] = [
{ role: 'system', content: simpleImagineSystemPrompt },
{ role: 'user', content: 'Write a prompt, based on the following input.\n\n```\n' + messageText.slice(0, 1000) + '\n```\n' },
{ role: 'user', content: 'Write a minimum of 20-30 words prompt and up to the size of the input, based on the INPUT below.\n\nINPUT:\n' + messageText },
];
const chatResponse = await llmChatGenerateOrThrow(fastLLMId, instructions, 'draw-expand-prompt', contextRef, null, null);
const chatResponse = await llmChatGenerateOrThrow(chatLLMId, instructions, 'draw-expand-prompt', contextRef, null, null);
return chatResponse.content?.trim() ?? null;
} catch (error: any) {
console.error('imaginePromptFromText: fetch request error:', error);