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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user