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

@@ -519,21 +519,27 @@ export const heaps = {
}
// Tags API
export type TagKind = 'user' | 'object' | 'scene' | 'face_cluster'
export interface Tag {
id: string
name: string
color: string | null
kind: TagKind
source: string | null
representative_photo_id: string | null
photo_count: number
}
export const tags = {
list: async (): Promise<Tag[]> => {
const response = await api.get('/tags')
list: async (kind?: TagKind): Promise<Tag[]> => {
const params = kind ? { kind } : undefined
const response = await api.get('/tags', { params })
return response.data
},
create: async (name: string, color?: string): Promise<Tag> => {
const response = await api.post('/tags', { name, color })
create: async (name: string, color?: string, kind: TagKind = 'user'): Promise<Tag> => {
const response = await api.post('/tags', { name, color, kind })
return response.data
},
@@ -546,6 +552,11 @@ export const tags = {
await api.delete(`/tags/${tagId}`)
},
merge: async (sourceId: string, targetId: string): Promise<{ merged_into: string; target_name: string }> => {
const response = await api.post(`/tags/${sourceId}/merge`, { target_id: targetId })
return response.data
},
/** Add one or more tags to a photo. */
addToPhoto: async (photoId: string, tagIds: string[]) => {
const response = await api.post(`/photos/${photoId}/tags`, { tag_ids: tagIds })
@@ -558,6 +569,38 @@ export const tags = {
},
}
// Search API — hybrid FTS + semantic search
export interface SearchResult {
id: string
filename: string
filepath: string
media_type: string
width: number
height: number
taken_at: string | null
rating: number
color_label: string | null
thumb_small: string
thumb_medium: string
score: number
}
export const search = {
query: async (params: {
q?: string
filters?: {
tag_ids?: string[]
date_from?: string
date_to?: string
}
limit?: number
offset?: number
}): Promise<{ results: SearchResult[]; total: number }> => {
const response = await api.post('/photos/search', params)
return response.data
},
}
// Discard API
export const discard = {
list: async () => {