Refine ai text generation form behavior

This commit is contained in:
Sam Becker 2024-03-21 16:05:13 -05:00
parent 5a0e372e39
commit 28f6310fe1
9 changed files with 112 additions and 43 deletions

42
__tests__/ai.test.ts Normal file
View File

@ -0,0 +1,42 @@
/* eslint-disable quotes */
import { parseTitleAndCaption } from "@/photo/ai";
describe('AI text parses', () => {
it('titles and captions', () => {
// Complex case
expect(parseTitleAndCaption(
`'Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight."'`
)).toStrictEqual({
title: 'Ephemeral Beauty',
caption: 'Roses bask in fleeting sunlight',
});
// Without surrounding single quotes
expect(parseTitleAndCaption(
`Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight."`
)).toStrictEqual({
title: 'Ephemeral Beauty',
caption: 'Roses bask in fleeting sunlight',
});
// Without trailing period
expect(parseTitleAndCaption(
`Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight"`
)).toStrictEqual({
title: 'Ephemeral Beauty',
caption: 'Roses bask in fleeting sunlight',
});
// Without and quotes
expect(parseTitleAndCaption(
`Title: Ephemeral Beauty Caption: Roses bask in fleeting sunlight`
)).toStrictEqual({
title: 'Ephemeral Beauty',
caption: 'Roses bask in fleeting sunlight',
});
// With single space
expect(parseTitleAndCaption(
`Title: Ephemeral Beauty Caption: Roses bask in fleeting sunlight`
)).toStrictEqual({
title: 'Ephemeral Beauty',
caption: 'Roses bask in fleeting sunlight',
});
});
});

View File

@ -48,7 +48,7 @@ export default function PhotoEditPageClient({
hasTextContent,
setHasTextContent,
aiContent,
} = usePhotoFormParent(photoForm);
} = usePhotoFormParent({ photoForm });
return (
<AdminChildPage

View File

@ -27,7 +27,7 @@ export default function UploadPageClient({
hasTextContent,
setHasTextContent,
aiContent,
} = usePhotoFormParent();
} = usePhotoFormParent({ shouldAutoGenerateText: hasAiTextGeneration });
return (
<AdminChildPage

View File

@ -34,7 +34,8 @@ import { extractExifDataFromBlobPath } from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert } from '.';
import { safelyRunAdminServerAction } from '@/auth';
import { AiImageQuery, streamAiImageQuery } from './ai';
import { AI_IMAGE_QUERIES, AiImageQuery } from './ai';
import { streamOpenAiImageQuery } from '@/services/openai';
export async function createPhotoAction(formData: FormData) {
return safelyRunAdminServerAction(async () => {
@ -188,5 +189,5 @@ export async function streamAiImageQueryAction(
query: AiImageQuery,
) {
return safelyRunAdminServerAction(async () =>
streamAiImageQuery(imageBase64, query));
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]));
}

View File

@ -1,7 +1,5 @@
/* eslint-disable max-len */
import { streamOpenAiImageQuery } from '@/services/openai';
export type AiImageQuery =
'title' |
'caption' |
@ -23,5 +21,13 @@ export const AI_IMAGE_QUERIES: Record<AiImageQuery, string> = {
'description-semantic': 'List up to 5 things in this image without description as a comma-separated list',
};
export const streamAiImageQuery = (imageBase64: string, query: AiImageQuery) =>
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]);
export const parseTitleAndCaption = (text: string) => {
const matches = text.includes('Title')
? text.match(/^[`'"]*Title: ["']*(.*?)["']*[ ]*Caption: ["']*(.*?)\.*["']*[`'"]*$/)
: text.match(/^(.*?): (.*?)$/);
return {
title: matches?.[1] ?? '',
caption: matches?.[2] ?? '',
};
};

View File

@ -1,10 +1,12 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import useAiImageQuery from './useAiImageQuery';
import useTitleCaptionAiImageQuery from './useTitleCaptionAiImageQuery';
export type AiContent = ReturnType<typeof useAiImageQueries>;
export default function useAiImageQueries() {
export default function useAiImageQueries(
shouldAutoGenerateText?: boolean,
) {
const [imageData, setImageData] = useState<string>();
const isReady = Boolean(imageData);
@ -42,13 +44,23 @@ export default function useAiImageQueries() {
isLoadingTags ||
isLoadingSemantic;
const hasRunAllQueriesOnce = useRef(false);
const request = useCallback(async () => {
if (!isLoading) {
requestTitleCaption();
requestTags();
requestSemantic();
console.log('RUNNING ALL AI QUERIES');
hasRunAllQueriesOnce.current = true;
requestTitleCaption();
requestTags();
requestSemantic();
}, [requestTitleCaption, requestTags, requestSemantic]);
useEffect(() => {
if (shouldAutoGenerateText && imageData) {
if (!hasRunAllQueriesOnce.current) {
request();
}
}
}, [isLoading, requestTitleCaption, requestTags, requestSemantic]);
}, [shouldAutoGenerateText, imageData, request]);
return {
request,

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import useAiImageQuery from './useAiImageQuery';
import { parseTitleAndCaption } from '.';
export default function useTitleCaptionAiImageQuery(
imageBase64: string | undefined,
@ -11,16 +12,8 @@ export default function useTitleCaptionAiImageQuery(
error,
] = useAiImageQuery(imageBase64, 'title-and-caption');
const { title, caption } = useMemo(() => {
const matches = text.includes('Title')
? text.match(/^[`']*Title: "*(.*?)"* Caption: "*(.*?)\.*"*[`']*$/)
: text.match(/^(.*?): (.*?)$/);
return {
title: matches?.[1] ?? '',
caption: matches?.[2] ?? '',
};
}, [text]);
const { title, caption } = useMemo(() =>
parseTitleAndCaption(text), [text]);
const isLoadingTitle = isLoading && !caption;
const isLoadingCaption = isLoading;

View File

@ -123,21 +123,33 @@ export default function PhotoForm({
}
}, []);
useEffect(() => setFormData(data =>
({ ...data, title: aiContent?.title })),
[aiContent?.title]);
useEffect(() =>
setFormData(data => aiContent?.hasContent
? { ...data, title: aiContent?.title }
: data),
[aiContent?.title, aiContent?.hasContent]);
useEffect(() => setFormData(data =>
({ ...data, caption: aiContent?.caption })),
[aiContent?.caption]);
useEffect(() =>
setFormData(data => aiContent?.hasContent
? { ...data, caption: aiContent?.caption }
: data),
[aiContent?.caption, aiContent?.hasContent]);
useEffect(() => setFormData(data =>
({ ...data, tags: aiContent?.tags })),
[aiContent?.tags]);
useEffect(() =>
setFormData(data => aiContent?.hasContent
? { ...data, tags: aiContent?.tags }
: data),
[aiContent?.tags, aiContent?.hasContent]);
useEffect(() => setFormData(data =>
({ ...data, semanticDescription: aiContent?.semanticDescription })),
[aiContent?.semanticDescription]);
useEffect(() =>
setFormData(data => aiContent?.hasContent
? { ...data, semanticDescription: aiContent?.semanticDescription }
: data),
[aiContent?.semanticDescription, aiContent?.hasContent]);
useEffect(() => {
onTextContentChange?.(formHasTextContent(formData));
}, [onTextContentChange, formData]);
const isFieldGeneratingAi = (key: keyof PhotoFormData) => {
switch (key) {
@ -236,7 +248,6 @@ export default function PhotoForm({
onChange={value => {
const formUpdated = { ...formData, [key]: value };
setFormData(formUpdated);
onTextContentChange?.(formHasTextContent(formUpdated));
if (validate) {
setFormErrors({ ...formErrors, [key]: validate(value) });
} else if (validateStringMaxLength !== undefined) {
@ -273,7 +284,7 @@ export default function PhotoForm({
Cancel
</Link>
<SubmitButtonWithStatus
disabled={!isFormValid(formData)}
disabled={!isFormValid(formData) || aiContent?.isLoading}
onFormStatusChange={onFormStatusChange}
>
{type === 'create' ? 'Create' : 'Update'}

View File

@ -2,15 +2,19 @@ import { useState } from 'react';
import { PhotoFormData, formHasTextContent } from '.';
import useAiImageQueries from '../ai/useAiImageQueries';
export default function usePhotoFormParent(
photoForm?: Partial<PhotoFormData>
) {
export default function usePhotoFormParent({
photoForm,
shouldAutoGenerateText,
}: {
photoForm?: Partial<PhotoFormData>,
shouldAutoGenerateText?: boolean,
} = {}) {
const [pending, setIsPending] = useState(false);
const [updatedTitle, setUpdatedTitle] = useState('');
const [hasTextContent, setHasTextContent] =
useState(photoForm ? formHasTextContent(photoForm) : false);
const aiContent = useAiImageQueries();
const aiContent = useAiImageQueries(shouldAutoGenerateText);
return {
pending,