feat: wire frontend to vision pipeline search and tags

- Add search API client (POST /photos/search) and useSearchQuery hook
  for hybrid FTS + semantic search with RRF ranking
- Extend Tag type with kind, source, representative_photo_id fields
- Add tags.merge() API method
- Update useTagsQuery to accept optional kind filter
- Add People section to sidebar (face clusters from GET /tags?kind=face_cluster)
- Sidebar Tags count now shows user tags only; People shows face clusters

The existing GET /photos?q= flow is preserved for browsing; the new
search hook activates when the search box has a non-empty query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:19:47 +02:00
parent ad007e4cd4
commit 29177f0c1a
4 changed files with 98 additions and 10 deletions

View File

@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query'
import { search, type SearchResult } from '../services/api'
import { useFilterStore } from '../store/filterStore'
/**
* Hybrid search hook — fires POST /photos/search when the user has a
* non-empty search query. Returns results ranked by RRF (FTS + semantic).
*
* When `q` is empty, this hook is disabled and returns no data — the
* normal usePhotosQuery takes over for browse mode.
*/
export function useSearchQuery() {
const q = useFilterStore((s) => s.q)
const tagIds = useFilterStore((s) => s.tagIds)
const dateFrom = useFilterStore((s) => s.dateFrom)
const dateTo = useFilterStore((s) => s.dateTo)
const hasQuery = q.trim().length > 0
return useQuery<SearchResult[]>({
queryKey: ['search', q, tagIds, dateFrom, dateTo],
queryFn: async () => {
const resp = await search.query({
q: q.trim(),
filters: {
tag_ids: tagIds.length > 0 ? tagIds : undefined,
date_from: dateFrom ?? undefined,
date_to: dateTo ?? undefined,
},
limit: 200,
})
return resp.results
},
enabled: hasQuery,
staleTime: 30_000,
})
}

View File

@@ -1,12 +1,12 @@
import { useQuery } from '@tanstack/react-query'
import { tags as tagsApi, type Tag } from '../services/api'
import { tags as tagsApi, type Tag, type TagKind } from '../services/api'
export const TAGS_QUERY_KEY = ['tags'] as const
export function useTagsQuery() {
export function useTagsQuery(kind?: TagKind) {
return useQuery<Tag[]>({
queryKey: TAGS_QUERY_KEY,
queryFn: tagsApi.list,
queryKey: kind ? ['tags', kind] : TAGS_QUERY_KEY,
queryFn: () => tagsApi.list(kind),
staleTime: 30_000,
})
}