diff --git a/src/helpers/gemini.helper.ts b/src/helpers/gemini.helper.ts index d75a58e..bfe853e 100644 --- a/src/helpers/gemini.helper.ts +++ b/src/helpers/gemini.helper.ts @@ -57,7 +57,8 @@ const responseSchema = { export const parseFindQuery = async (query: string): Promise => { const prompt = ` - Parse the following query and return the query and tags: ${query} + Parse the following query and return the query and tags: ${query}. + Dont include any information that are not intentionally provided in the query. Additional Information For Parsing: today's date is ${new Date().toISOString().split('T')[0]} `; diff --git a/src/pages/api/find/search.ts b/src/pages/api/find/search.ts index b76fe55..07c1668 100644 --- a/src/pages/api/find/search.ts +++ b/src/pages/api/find/search.ts @@ -3,6 +3,7 @@ import { ENV } from '@/config/environment'; import { getCurrentUser } from '@/handlers/serverUtils/user.utils'; import { parseFindQuery } from '@/helpers/gemini.helper'; import { assetFaces, assets, person } from '@/schema'; +import { Person } from '@/schema/person.schema'; import { and, eq, inArray } from 'drizzle-orm'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -10,8 +11,16 @@ export default async function search(req: NextApiRequest, res: NextApiResponse) const { query } = req.body; const currentUser = await getCurrentUser(req); const parsedQuery = await parseFindQuery(query as string); + const { personIds } = parsedQuery; const url = ENV.IMMICH_URL + "/api/search/smart"; + let dbPeople: Person[] = []; + if (personIds) { + dbPeople = await db.select().from(person).where( + inArray(person.id, personIds) + ); + } + return fetch(url, { method: 'POST', body: JSON.stringify(parsedQuery), @@ -33,7 +42,10 @@ export default async function search(req: NextApiRequest, res: NextApiResponse) .then(data => { res.status(200).json({ assets: data.assets.items, - filters: parsedQuery + filters: { + ...parsedQuery, + personIds: dbPeople.map((person) => person.name) + }, }); }).catch(err => { res.status(500).json({ error: err.message }); diff --git a/src/pages/find/index.tsx b/src/pages/find/index.tsx index 758e97d..83aaa13 100644 --- a/src/pages/find/index.tsx +++ b/src/pages/find/index.tsx @@ -7,14 +7,27 @@ import { ASSET_PREVIEW_PATH, ASSET_THUMBNAIL_PATH } from '@/config/routes'; import { useConfig } from '@/contexts/ConfigContext'; import { findAssets } from '@/handlers/api/asset.handler'; import { IAsset } from '@/types/asset'; -import { Captions, Megaphone, Search, Speaker, TriangleAlert, WandSparkles } from 'lucide-react'; +import { ArrowUpRight, Captions, Megaphone, Search, Speaker, TriangleAlert, WandSparkles } from 'lucide-react'; import Image from 'next/image'; import React, { useMemo, useState } from 'react' import Lightbox from 'yet-another-react-lightbox'; import { Button } from "@/components/ui/button"; import Link from "next/link"; +interface IFindFilters { + [key: string]: string; +} +const FILTER_KEY_MAP = { + "city": "City", + "state": "State", + "country": "Country", + "takenAfter": "Taken After", + "takenBefore": "Taken Before", + "size": "Size", + "model": "Model", + "personIds": "People", +} export default function FindPage() { const [index, setIndex] = useState(-1); @@ -22,6 +35,7 @@ export default function FindPage() { const [query, setQuery] = useState(''); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({}); const slides = useMemo( () => @@ -33,19 +47,46 @@ export default function FindPage() { [assets] ); - + const appliedFilters: { + label: string; + value: string; + }[] = useMemo(() => { + return Object.entries(filters) + .filter(([_key, value]) => value !== undefined) + .map(([key, value]) => ({ + label: key, + value: Array.isArray(value) ? value.join(', ') : value, + })) + .filter((filter) => filter.label !== "query"); + }, [filters]); + const handleSearch = (query: string) => { setQuery(query); setLoading(true); findAssets(query) - .then(({ assets }) => { + .then(({ assets, filters: _filters }) => { setAssets(assets); + setFilters(_filters); }) .finally(() => { setLoading(false); }); } + const renderFilters = () => { + if (appliedFilters.length === 0) return null; + return ( +
+ {appliedFilters.map((filter) => ( +
+

{FILTER_KEY_MAP[filter.label as keyof typeof FILTER_KEY_MAP] || filter.label}

+

{filter.value}

+
+ ))} +
+ ) + } + const renderContent = () => { if (loading) { return
@@ -53,37 +94,46 @@ export default function FindPage() {
} else if (query.length === 0) { - return
- -

Search for photos in natural language

-

- Example: Sunset photos from last week. Use @ to search for photos of a specific person. -

-

- Power tools uses Google Gemini only for parsing your query. None of your data is sent to Gemini. -

-
+ return ( +
+ +

Search for photos in natural language

+

+ Example: Sunset photos from last week. Use @ to search for photos of a specific person. +

+

+ Power tools uses Google Gemini only for parsing your query. None of your data is sent to Gemini. +

+
+ ) } else if (assets.length === 0) { - return
- No results found + return
+ +

No results found for the below filters

+

+ {renderFilters()} +

+
} return ( <> - = 0} - index={index} - close={() => setIndex(-1)} - /> + = 0} + index={index} + close={() => setIndex(-1)} + /> + {renderFilters()}
{assets.map((asset, idx) => ( -
- - Open in Immich +
+ + Open +