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

@@ -17,6 +17,7 @@ import {
Pencil,
PanelLeftClose,
Settings,
Users,
} from 'lucide-react'
import clsx from 'clsx'
import { sourceFolders, photos as photosApi, type FolderTreeNode } from '../../services/api'
@@ -63,6 +64,7 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
const navigateToSection = useFilterStore((s) => s.navigateToSection)
const currentSection = useFilterStore((s) => s.currentSection)
const { data: allTags = [] } = useTagsQuery()
const { data: faceClusters = [] } = useTagsQuery('face_cluster')
const { data: stats } = useLibraryStatsQuery()
const [dropTargetId, setDropTargetId] = useState<string | null>(null)
@@ -255,6 +257,9 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
case 'tags':
navigateToSection('tags', { groupBy: 'tag' })
break
case 'people':
navigateToSection('people', { groupBy: 'tag' })
break
case 'colors':
navigateToSection('colors', { groupBy: 'color' })
break
@@ -349,8 +354,10 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
: undefined,
})
// Total tag count for the badge on the Tags entry.
const tagsTotalCount = allTags.reduce((sum, t) => sum + (t.photo_count || 0), 0)
// Total tag count for the badge on the Tags entry (user tags only).
const userTags = allTags.filter((t) => t.kind === 'user')
const tagsTotalCount = userTags.reduce((sum, t) => sum + (t.photo_count || 0), 0)
const peopleTotalCount = faceClusters.reduce((sum, t) => sum + (t.photo_count || 0), 0)
const libraryTree: TreeItem[] = [
{
@@ -361,6 +368,7 @@ export function LeftSidebar({ onCollapse, onOpenSettings }: LeftSidebarProps) {
{ id: 'all-photos', label: 'All Photos', icon: <Image className="h-4 w-4" />, count: stats?.all_photos ?? 0 },
{ id: 'rated', label: 'Rated', icon: <Star className="h-4 w-4" />, count: stats?.rated ?? 0 },
{ id: 'tags', label: 'Tags', icon: <TagIcon className="h-4 w-4" />, count: tagsTotalCount },
{ id: 'people', label: 'People', icon: <Users className="h-4 w-4" />, count: peopleTotalCount },
{ id: 'colors', label: 'Colors', icon: <Palette className="h-4 w-4" />, count: stats?.colored ?? 0 },
{ id: 'map', label: 'Map', icon: <MapPin className="h-4 w-4" />, count: stats?.with_gps ?? 0 },
{ id: 'duplicates', label: 'Duplicates', icon: <Copy className="h-4 w-4" />, count: stats?.duplicates ?? 0 },