diff --git a/src/admin/AdminPhotosSyncClient.tsx b/src/admin/AdminPhotosSyncClient.tsx index b55ee092..94479816 100644 --- a/src/admin/AdminPhotosSyncClient.tsx +++ b/src/admin/AdminPhotosSyncClient.tsx @@ -2,7 +2,6 @@ import { Photo } from '@/photo'; import AdminPhotosTable from '@/admin/AdminPhotosTable'; -import LoaderButton from '@/components/primitives/LoaderButton'; import IconGrSync from '@/components/icons/IconGrSync'; import Note from '@/components/Note'; import AdminChildPage from '@/components/AdminChildPage'; @@ -12,6 +11,7 @@ import { syncPhotosAction } from '@/photo/actions'; import { useRouter } from 'next/navigation'; import ResponsiveText from '@/components/primitives/ResponsiveText'; import { LiaBroomSolid } from 'react-icons/lia'; +import ProgressButton from '@/components/primitives/ProgressButton'; const UPDATE_BATCH_SIZE_MAX = 4; @@ -37,7 +37,7 @@ export default function AdminPhotosSyncClient({ breadcrumb={ Needs Sync ({photos.length}) } - accessory={} hideTextOnMobile={false} @@ -68,7 +68,7 @@ export default function AdminPhotosSyncClient({ {arePhotoIdsSyncing ? 'Syncing' : 'Sync All'} - } + } >
{<> - + {photoHasSyncStatusText(photo) && - {children ?? } + {children ?? } ); } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 6513ba29..3ec326bd 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -414,7 +414,7 @@ export const syncPhotoAction = async (photoId: string) => semanticDescription: aiSemanticDescription, } = await generateAiImageQueries( imageResizedBase64, - AI_TEXT_AUTO_GENERATED_FIELDS, + photo.syncStatus.missingAiTextFields, ); const formDataFromPhoto = convertPhotoToFormData(photo); diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 246befa3..07e73777 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -599,7 +599,7 @@ const needsAiTextWhereClauses = ( switch (field) { case 'title': return `(title <> '') IS NOT TRUE`; case 'caption': return `(caption <> '') IS NOT TRUE`; - case 'tags': return `array_length(tags, 1) = 0`; + case 'tags': return `(tags IS NULL OR array_length(tags, 1) = 0)`; case 'semantic': return `(semantic_description <> '') IS NOT TRUE`; } }) diff --git a/src/photo/index.ts b/src/photo/index.ts index fba70537..a93c3675 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -97,7 +97,7 @@ export interface PhotoDb extends updatedAt: Date createdAt: Date takenAt: Date - tags?: string[] + tags: string[] | null } // Parsed db response diff --git a/src/photo/sync.ts b/src/photo/sync.ts index d28dcbce..657bce5c 100644 --- a/src/photo/sync.ts +++ b/src/photo/sync.ts @@ -1,10 +1,11 @@ import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { Photo, PhotoDb } from '.'; import { AI_TEXT_AUTO_GENERATED_FIELDS } from '@/app/config'; +import { AiAutoGeneratedField } from './ai'; export interface PhotoSyncStatus { isOutdated: boolean; - isMissingAiText: boolean; + missingAiTextFields: AiAutoGeneratedField[]; } export const SYNC_QUERY_LIMIT = 1000; @@ -19,32 +20,44 @@ const isPhotoOutdated = (photo: PhotoDb) => photo.make === MAKE_FUJIFILM ); -const doesPhotoNeedAiText = ({ +const getMissingAiTextFields = ({ title, caption, - tags = [], + tags, semanticDescription, -}: PhotoDb) => - (AI_TEXT_AUTO_GENERATED_FIELDS.includes('title') && !title) || - (AI_TEXT_AUTO_GENERATED_FIELDS.includes('caption') && !caption) || - (AI_TEXT_AUTO_GENERATED_FIELDS.includes('tags') && tags.length === 0) || - (AI_TEXT_AUTO_GENERATED_FIELDS.includes('semantic') && !semanticDescription); +}: PhotoDb | Photo): AiAutoGeneratedField[] => + AI_TEXT_AUTO_GENERATED_FIELDS.reduce((fields, field) => { + switch (field) { + case 'title': + return !title ? [...fields, 'title'] : fields; + case 'caption': + return !caption ? [...fields, 'caption'] : fields; + case 'tags': + return (tags ?? []).length === 0 ? [...fields, 'tags'] : fields; + case 'semantic': + return !semanticDescription ? [...fields, 'semantic'] : fields; + } + }, [] as AiAutoGeneratedField[]); export const generatePhotoSyncStatus = (photo: PhotoDb): PhotoSyncStatus => ({ isOutdated: isPhotoOutdated(photo), - isMissingAiText: doesPhotoNeedAiText(photo), + missingAiTextFields: getMissingAiTextFields(photo), }); export const photoHasSyncStatusText = (photo: Photo) => - photo.syncStatus.isOutdated || photo.syncStatus.isMissingAiText; + photo.syncStatus.isOutdated || + photo.syncStatus.missingAiTextFields.length > 0; export const photoSyncStatusText = (photo: Photo) => { - const { isOutdated, isMissingAiText } = photo.syncStatus; + const { isOutdated, missingAiTextFields } = photo.syncStatus; const text: string[] = []; if (isOutdated) { - text.push('Outdated'); - } else if (isMissingAiText) { - text.push('Missing AI Text'); + text.push('Outdated Data'); + } else if (missingAiTextFields.length > 0) { + const missingFieldsText = missingAiTextFields + .map(field => field.toLocaleUpperCase()) + .join(', '); + text.push(`Missing AI Text (${missingFieldsText})`); } return text.join(' and '); };