Add caption, semantic description to search

This commit is contained in:
Sam Becker 2024-03-21 22:37:04 -05:00
parent e21ed7942b
commit 8a03ea8217
9 changed files with 40 additions and 18 deletions

View File

@ -21,6 +21,7 @@
"headlessui", "headlessui",
"hgetall", "hgetall",
"hset", "hset",
"ILIKE",
"jpgs", "jpgs",
"Lightbox", "Lightbox",
"Makernote", "Makernote",

View File

@ -27,6 +27,7 @@ export type CommandKSection = {
accessory?: ReactNode accessory?: ReactNode
items: { items: {
label: string label: string
keywords?: string[]
annotation?: ReactNode annotation?: ReactNode
annotationAria?: string annotationAria?: string
accessory?: ReactNode accessory?: ReactNode
@ -157,8 +158,13 @@ export default function CommandKClient({
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
label="Global Command Menu" label="Global Command Menu"
filter={(value, search) => filter={(value, search, keywords) => {
value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0} const searchFormatted = search.trim().toLocaleLowerCase();
return (
value.toLocaleLowerCase().includes(searchFormatted) ||
keywords?.includes(searchFormatted)
) ? 1 : 0 ;
}}
loop loop
> >
<Modal <Modal
@ -223,16 +229,18 @@ export default function CommandKClient({
)} )}
> >
{items.map(({ {items.map(({
accessory,
label, label,
keywords,
annotation, annotation,
annotationAria, annotationAria,
accessory,
path, path,
action, action,
}) => }) =>
<Command.Item <Command.Item
key={`${heading} ${label}`} key={`${heading} ${label}`}
value={`${heading} ${label}`} value={`${heading} ${label}`}
keywords={keywords}
className={clsx( className={clsx(
'px-2', 'px-2',
accessory ? 'py-2' : 'py-1', accessory ? 'py-2' : 'py-1',

View File

@ -1,5 +1,6 @@
import { import {
Photo, Photo,
altTextForPhoto,
shouldShowCameraDataForPhoto, shouldShowCameraDataForPhoto,
shouldShowExifDataForPhoto, shouldShowExifDataForPhoto,
titleForPhoto, titleForPhoto,
@ -54,7 +55,7 @@ export default function PhotoLarge({
contentMain={ contentMain={
<ImageLarge <ImageLarge
className="w-full" className="w-full"
alt={titleForPhoto(photo)} alt={altTextForPhoto(photo)}
href={pathForPhoto(photo, primaryTag)} href={pathForPhoto(photo, primaryTag)}
src={photo.url} src={photo.url}
aspectRatio={photo.aspectRatio} aspectRatio={photo.aspectRatio}

View File

@ -1,4 +1,4 @@
import { Photo, titleForPhoto } from '.'; import { Photo, altTextForPhoto } from '.';
import ImageSmall from '@/components/ImageSmall'; import ImageSmall from '@/components/ImageSmall';
import Link from 'next/link'; import Link from 'next/link';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
@ -34,7 +34,7 @@ export default function PhotoSmall({
aspectRatio={photo.aspectRatio} aspectRatio={photo.aspectRatio}
blurData={photo.blurData} blurData={photo.blurData}
className="w-full" className="w-full"
alt={titleForPhoto(photo)} alt={altTextForPhoto(photo)}
/> />
</Link> </Link>
); );

View File

@ -1,4 +1,4 @@
import { Photo, titleForPhoto } from '.'; import { Photo, altTextForPhoto } from '.';
import ImageTiny from '@/components/ImageTiny'; import ImageTiny from '@/components/ImageTiny';
import Link from 'next/link'; import Link from 'next/link';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
@ -31,7 +31,7 @@ export default function PhotoTiny({
src={photo.url} src={photo.url}
aspectRatio={photo.aspectRatio} aspectRatio={photo.aspectRatio}
blurData={photo.blurData} blurData={photo.blurData}
alt={titleForPhoto(photo)} alt={altTextForPhoto(photo)}
/> />
</Link> </Link>
); );

View File

@ -47,7 +47,9 @@ export default function useAiImageQueries(
const hasRunAllQueriesOnce = useRef(false); const hasRunAllQueriesOnce = useRef(false);
const request = useCallback(async () => { const request = useCallback(async () => {
if (process.env.NODE_ENV === 'development') {
console.log('RUNNING ALL AI QUERIES'); console.log('RUNNING ALL AI QUERIES');
}
hasRunAllQueriesOnce.current = true; hasRunAllQueriesOnce.current = true;
requestTitleCaption(); requestTitleCaption();
requestTags(); requestTags();

View File

@ -168,6 +168,9 @@ export const translatePhotoId = (id: string) =>
export const titleForPhoto = (photo: Photo) => export const titleForPhoto = (photo: Photo) =>
photo.title || 'Untitled'; photo.title || 'Untitled';
export const altTextForPhoto = (photo: Photo) =>
photo.semanticDescription || titleForPhoto(photo);
export const photoLabelForCount = (count: number) => export const photoLabelForCount = (count: number) =>
count === 1 ? 'Photo' : 'Photos'; count === 1 ? 'Photo' : 'Photos';
@ -247,3 +250,9 @@ export const shouldShowCameraDataForPhoto = (photo: Photo) =>
export const shouldShowExifDataForPhoto = (photo: Photo) => export const shouldShowExifDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA && photoHasExifData(photo); SHOW_EXIF_DATA && photoHasExifData(photo);
export const getKeywordsForPhoto = (photo: Photo) =>
(photo.caption ?? '').split(' ')
.concat((photo.semanticDescription ?? '').split(' '))
.filter(Boolean)
.map(keyword => keyword.toLocaleLowerCase());

View File

@ -294,7 +294,7 @@ export type GetPhotosOptions = {
sortBy?: 'createdAt' | 'takenAt' | 'priority' sortBy?: 'createdAt' | 'takenAt' | 'priority'
limit?: number limit?: number
offset?: number offset?: number
title?: string query?: string
tag?: string tag?: string
camera?: Camera camera?: Camera
simulation?: FilmSimulation simulation?: FilmSimulation
@ -344,7 +344,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt', sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt',
limit = PHOTO_DEFAULT_LIMIT, limit = PHOTO_DEFAULT_LIMIT,
offset = 0, offset = 0,
title, query,
tag, tag,
camera, camera,
simulation, simulation,
@ -370,9 +370,10 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
wheres.push(`taken_at <= $${valueIndex++}`); wheres.push(`taken_at <= $${valueIndex++}`);
values.push(takenAfterInclusive.toISOString()); values.push(takenAfterInclusive.toISOString());
} }
if (title) { if (query) {
wheres.push(`LOWER(title) LIKE $${valueIndex++}`); // eslint-disable-next-line max-len
values.push(`%${title.toLowerCase()}%`); wheres.push(`CONCAT(title, ' ', caption, ' ', semantic_description) ILIKE $${valueIndex++}`);
values.push(`%${query.toLocaleLowerCase()}%`);
} }
if (tag) { if (tag) {
wheres.push(`$${valueIndex++}=ANY(tags)`); wheres.push(`$${valueIndex++}=ANY(tags)`);

View File

@ -19,7 +19,7 @@ import {
import { formatCameraText } from '@/camera'; import { formatCameraText } from '@/camera';
import { authCached } from '@/auth/cache'; import { authCached } from '@/auth/cache';
import { getPhotos } from '@/services/vercel-postgres'; import { getPhotos } from '@/services/vercel-postgres';
import { photoQuantityText, titleForPhoto } from '@/photo'; import { getKeywordsForPhoto, photoQuantityText, titleForPhoto } from '@/photo';
import PhotoTiny from '@/photo/PhotoTiny'; import PhotoTiny from '@/photo/PhotoTiny';
import { formatDate } from '@/utility/date'; import { formatDate } from '@/utility/date';
import { formatCount, formatCountDescriptive } from '@/utility/string'; import { formatCount, formatCountDescriptive } from '@/utility/string';
@ -139,15 +139,14 @@ export default async function CommandK() {
]} ]}
onQueryChange={async (query) => { onQueryChange={async (query) => {
'use server'; 'use server';
const photos = (await getPhotos({ title: query, limit: 10 })) const photos = (await getPhotos({ query, limit: 10 }));
.filter(({ title }) => Boolean(title));
return photos.length > 0 return photos.length > 0
? [{ ? [{
heading: 'Photos', heading: 'Photos',
accessory: <TbPhoto size={14} />, accessory: <TbPhoto size={14} />,
items: photos.map(photo => ({ items: photos.map(photo => ({
accessory: <PhotoTiny photo={photo} />,
label: titleForPhoto(photo), label: titleForPhoto(photo),
keywords: getKeywordsForPhoto(photo),
annotation: <> annotation: <>
<span className="hidden sm:inline-block"> <span className="hidden sm:inline-block">
{formatDate(photo.takenAt)} {formatDate(photo.takenAt)}
@ -156,6 +155,7 @@ export default async function CommandK() {
{formatDate(photo.takenAt, true)} {formatDate(photo.takenAt, true)}
</span> </span>
</>, </>,
accessory: <PhotoTiny photo={photo} />,
path: pathForPhoto(photo), path: pathForPhoto(photo),
})), })),
}] }]