Wire up page-level AI streaming
This commit is contained in:
parent
1371a8dcc4
commit
e2e8c8edda
@ -12,6 +12,9 @@ import IconGrSync from '@/site/IconGrSync';
|
|||||||
import { getExifDataAction } from './actions';
|
import { getExifDataAction } from './actions';
|
||||||
import { Tags } from '@/tag';
|
import { Tags } from '@/tag';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import useImageQueries from './ai/useImageQueries';
|
||||||
|
import { HiSparkles } from 'react-icons/hi';
|
||||||
|
import Spinner from '@/components/Spinner';
|
||||||
|
|
||||||
export default function PhotoEditPageClient({
|
export default function PhotoEditPageClient({
|
||||||
photo,
|
photo,
|
||||||
@ -37,6 +40,8 @@ export default function PhotoEditPageClient({
|
|||||||
seedExifData,
|
seedExifData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const aiContent = useImageQueries();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminChildPage
|
<AdminChildPage
|
||||||
backPath={PATH_ADMIN_PHOTOS}
|
backPath={PATH_ADMIN_PHOTOS}
|
||||||
@ -45,16 +50,27 @@ export default function PhotoEditPageClient({
|
|||||||
? updatedTitle
|
? updatedTitle
|
||||||
: photo.title || photo.id}
|
: photo.title || photo.id}
|
||||||
accessory={
|
accessory={
|
||||||
<form action={action}>
|
<div className="flex gap-2">
|
||||||
<input name="photoUrl" value={photo.url} hidden readOnly />
|
<button
|
||||||
<SubmitButtonWithStatus
|
className="min-w-[3.25rem] flex justify-center"
|
||||||
icon={<IconGrSync
|
onClick={aiContent.request}
|
||||||
className="translate-y-[-1px] sm:mr-[4px]"
|
disabled={!aiContent.isReady || aiContent.isLoading}
|
||||||
/>}
|
|
||||||
>
|
>
|
||||||
EXIF
|
{aiContent.isLoading
|
||||||
</SubmitButtonWithStatus>
|
? <Spinner />
|
||||||
</form>}
|
: <HiSparkles size={16} />}
|
||||||
|
</button>
|
||||||
|
<form action={action}>
|
||||||
|
<input name="photoUrl" value={photo.url} hidden readOnly />
|
||||||
|
<SubmitButtonWithStatus
|
||||||
|
icon={<IconGrSync
|
||||||
|
className="translate-y-[-1px] sm:mr-[4px]"
|
||||||
|
/>}
|
||||||
|
>
|
||||||
|
EXIF
|
||||||
|
</SubmitButtonWithStatus>
|
||||||
|
</form>
|
||||||
|
</div>}
|
||||||
isLoading={pending}
|
isLoading={pending}
|
||||||
>
|
>
|
||||||
<PhotoForm
|
<PhotoForm
|
||||||
@ -64,7 +80,7 @@ export default function PhotoEditPageClient({
|
|||||||
? updatedExifData
|
? updatedExifData
|
||||||
: undefined}
|
: undefined}
|
||||||
uniqueTags={uniqueTags}
|
uniqueTags={uniqueTags}
|
||||||
aiTextGeneration={aiTextGeneration}
|
aiContent={aiTextGeneration ? aiContent : undefined}
|
||||||
onTitleChange={setUpdatedTitle}
|
onTitleChange={setUpdatedTitle}
|
||||||
onFormStatusChange={setIsPending}
|
onFormStatusChange={setIsPending}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { extractExifDataFromBlobPath } from './server';
|
|||||||
import { TAG_FAVS, isTagFavs } from '@/tag';
|
import { TAG_FAVS, isTagFavs } from '@/tag';
|
||||||
import { convertPhotoToPhotoDbInsert } from '.';
|
import { convertPhotoToPhotoDbInsert } from '.';
|
||||||
import { safelyRunAdminServerAction } from '@/auth';
|
import { safelyRunAdminServerAction } from '@/auth';
|
||||||
import { ImageQuery, streamImageQuery } from './ai';
|
import { AiImageQuery, streamAiImageQuery } from './ai';
|
||||||
|
|
||||||
export async function createPhotoAction(formData: FormData) {
|
export async function createPhotoAction(formData: FormData) {
|
||||||
return safelyRunAdminServerAction(async () => {
|
return safelyRunAdminServerAction(async () => {
|
||||||
@ -183,10 +183,10 @@ export async function syncCacheAction() {
|
|||||||
return safelyRunAdminServerAction(revalidateAllKeysAndPaths);
|
return safelyRunAdminServerAction(revalidateAllKeysAndPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamImageQueryAction(
|
export async function streamAiImageQueryAction(
|
||||||
imageBase64: string,
|
imageBase64: string,
|
||||||
query: ImageQuery,
|
query: AiImageQuery,
|
||||||
) {
|
) {
|
||||||
return safelyRunAdminServerAction(async () =>
|
return safelyRunAdminServerAction(async () =>
|
||||||
streamImageQuery(imageBase64, query));
|
streamAiImageQuery(imageBase64, query));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,27 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
|
||||||
import { streamOpenAiImageQuery } from '@/services/openai';
|
import { streamOpenAiImageQuery } from '@/services/openai';
|
||||||
|
|
||||||
export type ImageQuery =
|
export type AiImageQuery =
|
||||||
'title' |
|
'title' |
|
||||||
'caption' |
|
'caption' |
|
||||||
|
'title-and-caption' |
|
||||||
'tags' |
|
'tags' |
|
||||||
'descriptionSmall' |
|
'description-small' |
|
||||||
'descriptionMedium' |
|
'description' |
|
||||||
'descriptionLarge' |
|
'description-large' |
|
||||||
'rich' |
|
|
||||||
'semantic';
|
'semantic';
|
||||||
|
|
||||||
export const IMAGE_QUERIES: Record<ImageQuery, string> = {
|
export const AI_IMAGE_QUERIES: Record<AiImageQuery, string> = {
|
||||||
// title: 'Provide a short title for this image',
|
'title': 'Provide a short title for this image in 3 words or less',
|
||||||
title: 'Provide a short title for this image in 3 words or less',
|
'caption': 'What is a pithy caption for this image in 8 words or less?',
|
||||||
caption: 'What is a pithy caption for this image in 8 words or less?',
|
'title-and-caption': 'Write a short title and pithy caption of 8 words or less for this image, using the format Title: "title" Caption: "caption"',
|
||||||
// eslint-disable-next-line max-len
|
'tags': 'Describe this image three or less comma-separated keywords with no adjective or adverbs',
|
||||||
tags: 'Describe this image three or less comma-separated keywords with no adjective or adverbs',
|
'description-small': 'Describe this image succinctly',
|
||||||
descriptionSmall: 'Describe this image succinctly',
|
'description': 'Describe this image',
|
||||||
descriptionMedium: 'Describe this image',
|
'description-large': 'Describe this image in detail',
|
||||||
descriptionLarge: 'Describe this image in detail',
|
'semantic': 'List up to 5 things in this image without description as a comma-separated list',
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
rich: 'What is a short title and pithy caption of 8 words or less for this image?',
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
semantic: 'List up to 5 things in this image without description as a comma-separated list',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const streamImageQuery = (imageBase64: string, query: ImageQuery) =>
|
export const streamAiImageQuery = (imageBase64: string, query: AiImageQuery) =>
|
||||||
streamOpenAiImageQuery(imageBase64, IMAGE_QUERIES[query]);
|
streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query]);
|
||||||
|
|||||||
55
src/photo/ai/useImageQueries.ts
Normal file
55
src/photo/ai/useImageQueries.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import useImageQuery from './useImageQuery';
|
||||||
|
import useTitleCaptionImageQuery from './useTitleCaptionImageQuery';
|
||||||
|
|
||||||
|
export type AiContent = ReturnType<typeof useImageQueries>;
|
||||||
|
|
||||||
|
export default function useImageQueries() {
|
||||||
|
const [imageData, setImageData] = useState<string>();
|
||||||
|
|
||||||
|
const isReady = Boolean(imageData);
|
||||||
|
|
||||||
|
const [
|
||||||
|
requestTitleCaption,
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
isLoadingTitleCaption,
|
||||||
|
] = useTitleCaptionImageQuery(imageData);
|
||||||
|
|
||||||
|
const [
|
||||||
|
requestTags,
|
||||||
|
tags,
|
||||||
|
isLoadingTags,
|
||||||
|
] = useImageQuery(imageData, 'tags');
|
||||||
|
|
||||||
|
const [
|
||||||
|
requestSemantic,
|
||||||
|
semantic,
|
||||||
|
isLoadingSemantic,
|
||||||
|
] = useImageQuery(imageData, 'semantic');
|
||||||
|
|
||||||
|
const isLoading = isLoadingTitleCaption || isLoadingTags || isLoadingSemantic;
|
||||||
|
|
||||||
|
const request = useCallback(async () => {
|
||||||
|
if (!isLoading) {
|
||||||
|
console.log('REQUESTING ALL IMAGE QUERIES');
|
||||||
|
requestTitleCaption();
|
||||||
|
requestTags();
|
||||||
|
requestSemantic();
|
||||||
|
}
|
||||||
|
}, [isLoading, requestTitleCaption, requestTags, requestSemantic]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
tags,
|
||||||
|
semantic,
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
isLoadingTitleCaption,
|
||||||
|
isLoadingTags,
|
||||||
|
isLoadingSemantic,
|
||||||
|
setImageData,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { streamImageQueryAction } from '../actions';
|
import { streamAiImageQueryAction } from '../actions';
|
||||||
import { readStreamableValue } from 'ai/rsc';
|
import { readStreamableValue } from 'ai/rsc';
|
||||||
import { ImageQuery } from '.';
|
import { AiImageQuery } from '.';
|
||||||
|
|
||||||
export default function useImageQuery(
|
export default function useImageQuery(
|
||||||
imageBase64: string | undefined,
|
imageBase64: string | undefined,
|
||||||
query: ImageQuery,
|
query: AiImageQuery,
|
||||||
) {
|
) {
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
@ -15,12 +15,12 @@ export default function useImageQuery(
|
|||||||
if (imageBase64) {
|
if (imageBase64) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const textStream = await streamImageQueryAction(
|
const textStream = await streamAiImageQueryAction(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
for await (const text of readStreamableValue(textStream)) {
|
for await (const text of readStreamableValue(textStream)) {
|
||||||
setText(text ?? '');
|
setText((text ?? '').replaceAll('\n', ' '));
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
32
src/photo/ai/useTitleCaptionImageQuery.ts
Normal file
32
src/photo/ai/useTitleCaptionImageQuery.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useImageQuery from './useImageQuery';
|
||||||
|
|
||||||
|
export default function useTitleCaptionImageQuery(
|
||||||
|
imageBase64: string | undefined,
|
||||||
|
) {
|
||||||
|
const [
|
||||||
|
request,
|
||||||
|
text,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
] = useImageQuery(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]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
request,
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
@ -25,8 +25,7 @@ import ImageBlurFallback from '@/components/ImageBlurFallback';
|
|||||||
import { BLUR_ENABLED } from '@/site/config';
|
import { BLUR_ENABLED } from '@/site/config';
|
||||||
import { Tags, sortTagsObjectWithoutFavs } from '@/tag';
|
import { Tags, sortTagsObjectWithoutFavs } from '@/tag';
|
||||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||||
import Spinner from '@/components/Spinner';
|
import { AiContent } from '../ai/useImageQueries';
|
||||||
import useImageQuery from '../ai/useImageQuery';
|
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 300;
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ export default function PhotoForm({
|
|||||||
updatedExifData,
|
updatedExifData,
|
||||||
type = 'create',
|
type = 'create',
|
||||||
uniqueTags,
|
uniqueTags,
|
||||||
aiTextGeneration,
|
aiContent,
|
||||||
debugBlur,
|
debugBlur,
|
||||||
onTitleChange,
|
onTitleChange,
|
||||||
onFormStatusChange,
|
onFormStatusChange,
|
||||||
@ -44,7 +43,8 @@ export default function PhotoForm({
|
|||||||
updatedExifData?: Partial<PhotoFormData>
|
updatedExifData?: Partial<PhotoFormData>
|
||||||
type?: 'create' | 'edit'
|
type?: 'create' | 'edit'
|
||||||
uniqueTags?: Tags
|
uniqueTags?: Tags
|
||||||
aiTextGeneration?: boolean
|
aiContent?: AiContent
|
||||||
|
setImageData?: (imageData: string) => void
|
||||||
debugBlur?: boolean
|
debugBlur?: boolean
|
||||||
onTitleChange?: (updatedTitle: string) => void
|
onTitleChange?: (updatedTitle: string) => void
|
||||||
onFormStatusChange?: (pending: boolean) => void
|
onFormStatusChange?: (pending: boolean) => void
|
||||||
@ -55,8 +55,6 @@ export default function PhotoForm({
|
|||||||
useState(getFormErrors(initialPhotoForm));
|
useState(getFormErrors(initialPhotoForm));
|
||||||
const [blurError, setBlurError] =
|
const [blurError, setBlurError] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
const [imageData, setImageData] =
|
|
||||||
useState<string>();
|
|
||||||
|
|
||||||
// Update form when EXIF data
|
// Update form when EXIF data
|
||||||
// is refreshed by parent
|
// is refreshed by parent
|
||||||
@ -122,123 +120,12 @@ export default function PhotoForm({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const [
|
|
||||||
// requestTitle,
|
|
||||||
// title,
|
|
||||||
// isLoadingTitle,
|
|
||||||
// errorTitle,
|
|
||||||
// ] = useImageQuery(imageData, 'title');
|
|
||||||
|
|
||||||
// const [
|
|
||||||
// requestCaption,
|
|
||||||
// caption,
|
|
||||||
// isLoadingCaption,
|
|
||||||
// errorCaption,
|
|
||||||
// ] = useImageQuery(imageData, 'caption');
|
|
||||||
|
|
||||||
const [
|
|
||||||
requestTags,
|
|
||||||
tags,
|
|
||||||
isLoadingTags,
|
|
||||||
errorTags,
|
|
||||||
] = useImageQuery(imageData, 'tags');
|
|
||||||
|
|
||||||
const [
|
|
||||||
requestRich,
|
|
||||||
rich,
|
|
||||||
isLoadingRich,
|
|
||||||
errorRich,
|
|
||||||
] = useImageQuery(imageData, 'rich');
|
|
||||||
|
|
||||||
// const [
|
|
||||||
// requestDescriptionSmall,
|
|
||||||
// descriptionSmall,
|
|
||||||
// isLoadingDescriptionSmall,
|
|
||||||
// errorDescriptionSmall,
|
|
||||||
// ] = useImageQuery(imageData, 'descriptionSmall');
|
|
||||||
|
|
||||||
const [
|
|
||||||
requestSemantic,
|
|
||||||
semantic,
|
|
||||||
isLoadingSemantic,
|
|
||||||
errorSemantic,
|
|
||||||
] = useImageQuery(imageData, 'semantic');
|
|
||||||
|
|
||||||
const renderAiButton = (
|
|
||||||
label: string,
|
|
||||||
onClick: () => void,
|
|
||||||
isLoading: boolean,
|
|
||||||
error?: any,
|
|
||||||
) =>
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={!imageData || isLoading}
|
|
||||||
className={clsx(
|
|
||||||
'flex gap-2 items-center justify-center',
|
|
||||||
'disabled:opacity-50 text-sm px-2.5 min-h-0 py-1.5',
|
|
||||||
Boolean(error) && 'error text-error',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<span className="min-w-4">
|
|
||||||
{isLoading
|
|
||||||
? <Spinner className="translate-y-[1.5px]" />
|
|
||||||
: <>✨</>}
|
|
||||||
</span>
|
|
||||||
</button>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-[38rem]">
|
<div className="space-y-8 max-w-[38rem]">
|
||||||
{blurError &&
|
{debugBlur && blurError &&
|
||||||
<div className="border error text-error rounded-md px-2 py-1">
|
<div className="border error text-error rounded-md px-2 py-1">
|
||||||
{blurError}
|
{blurError}
|
||||||
</div>}
|
</div>}
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{/* {renderAiButton(
|
|
||||||
'Title',
|
|
||||||
requestTitle,
|
|
||||||
isLoadingTitle,
|
|
||||||
errorTitle,
|
|
||||||
)}
|
|
||||||
{renderAiButton(
|
|
||||||
'Caption',
|
|
||||||
requestCaption,
|
|
||||||
isLoadingCaption,
|
|
||||||
errorCaption,
|
|
||||||
)}
|
|
||||||
{renderAiButton(
|
|
||||||
'Tags',
|
|
||||||
requestTags,
|
|
||||||
isLoadingTags,
|
|
||||||
errorTags,
|
|
||||||
)} */}
|
|
||||||
{renderAiButton(
|
|
||||||
'Rich',
|
|
||||||
requestRich,
|
|
||||||
isLoadingRich,
|
|
||||||
errorRich,
|
|
||||||
)}
|
|
||||||
{renderAiButton(
|
|
||||||
'Tags',
|
|
||||||
requestTags,
|
|
||||||
isLoadingTags,
|
|
||||||
errorTags,
|
|
||||||
)}
|
|
||||||
{renderAiButton(
|
|
||||||
'Semantic',
|
|
||||||
requestSemantic,
|
|
||||||
isLoadingSemantic,
|
|
||||||
errorSemantic,
|
|
||||||
)}
|
|
||||||
{/* {renderAiButton(
|
|
||||||
'Description',
|
|
||||||
requestDescriptionSmall,
|
|
||||||
isLoadingDescriptionSmall,
|
|
||||||
errorDescriptionSmall,
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<ImageBlurFallback
|
<ImageBlurFallback
|
||||||
alt="Upload"
|
alt="Upload"
|
||||||
@ -255,7 +142,7 @@ export default function PhotoForm({
|
|||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onLoad={setImageData}
|
onLoad={aiContent?.setImageData}
|
||||||
onCapture={updateBlurData}
|
onCapture={updateBlurData}
|
||||||
onError={setBlurError}
|
onError={setBlurError}
|
||||||
/>
|
/>
|
||||||
@ -271,48 +158,10 @@ export default function PhotoForm({
|
|||||||
height={height}
|
height={height}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{/* <p>
|
<div>Title: {aiContent?.title}</div>
|
||||||
✨ TITLE: {title} {isLoadingTitle && <>
|
<div>Caption: {aiContent?.caption}</div>
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
<div>Tags: {aiContent?.tags}</div>
|
||||||
<Spinner />
|
<div>Semantic: {aiContent?.semantic}</div>
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
✨ CAPTION: {caption} {isLoadingCaption && <>
|
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p> */}
|
|
||||||
<p>
|
|
||||||
✨ RICH: {rich} {isLoadingRich && <>
|
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
✨ TAGS: {tags} {isLoadingTags && <>
|
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
✨ SEMANTIC: {semantic} {isLoadingSemantic && <>
|
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p>
|
|
||||||
{/* <p>
|
|
||||||
✨ DESCRIPTION: {descriptionSmall} {isLoadingDescriptionSmall && <>
|
|
||||||
<span className="inline-flex translate-y-[1.5px]">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</p> */}
|
|
||||||
<form
|
<form
|
||||||
action={type === 'create' ? createPhotoAction : updatePhotoAction}
|
action={type === 'create' ? createPhotoAction : updatePhotoAction}
|
||||||
onSubmit={() => blur()}
|
onSubmit={() => blur()}
|
||||||
@ -325,7 +174,7 @@ export default function PhotoForm({
|
|||||||
annotation: formatCount(count),
|
annotation: formatCount(count),
|
||||||
annotationAria: formatCountDescriptive(count, 'tagged'),
|
annotationAria: formatCountDescriptive(count, 'tagged'),
|
||||||
})),
|
})),
|
||||||
aiTextGeneration,
|
aiContent !== undefined,
|
||||||
)
|
)
|
||||||
.map(([key, {
|
.map(([key, {
|
||||||
label,
|
label,
|
||||||
|
|||||||
@ -72,12 +72,6 @@ const FORM_METADATA = (
|
|||||||
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
||||||
shouldHide: ({ title, caption }) => !title && !caption,
|
shouldHide: ({ title, caption }) => !title && !caption,
|
||||||
},
|
},
|
||||||
semanticDescription: {
|
|
||||||
label: 'semantic description',
|
|
||||||
capitalize: true,
|
|
||||||
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
|
||||||
hide: !aiTextGeneration,
|
|
||||||
},
|
|
||||||
tags: {
|
tags: {
|
||||||
label: 'tags',
|
label: 'tags',
|
||||||
tagOptions,
|
tagOptions,
|
||||||
@ -85,6 +79,12 @@ const FORM_METADATA = (
|
|||||||
? `'${TAG_FAVS}' is a reserved tag`
|
? `'${TAG_FAVS}' is a reserved tag`
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
semanticDescription: {
|
||||||
|
label: 'semantic description',
|
||||||
|
capitalize: true,
|
||||||
|
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
||||||
|
hide: !aiTextGeneration,
|
||||||
|
},
|
||||||
id: { label: 'id', readOnly: true, hideIfEmpty: true },
|
id: { label: 'id', readOnly: true, hideIfEmpty: true },
|
||||||
blurData: {
|
blurData: {
|
||||||
label: 'blur data',
|
label: 'blur data',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user