# Analysis: Extract Faces from Tag UI and Navigate to Identify Page

## User Request
In Tag UI, when selecting a photo, extract faces from it (if processed) and jump to Identify page with only those faces as reference faces (for left panel), possibly in a new tab.

## Current State Analysis

### Tag UI (`frontend/src/pages/Tags.tsx`)
- **Photo Selection**: Photos can be selected via checkboxes (lines 585-600)
- **Photo Data Available**: 
  - `photo.id` - Photo ID
  - `photo.face_count` - Number of faces detected (line 651)
  - `photo.processed` - Whether photo has been processed (line 641)
- **Current Actions**: 
  - Tag management (add/remove tags)
  - Bulk tagging operations
  - No navigation to Identify page currently

### Identify Page (`frontend/src/pages/Identify.tsx`)
- **Face Loading**: Uses `facesApi.getUnidentified()` (line 86)
- **API Endpoint**: `/api/v1/faces/unidentified` 
- **Current Filters Supported**:
  - `page`, `page_size`
  - `min_quality`
  - `date_taken_from`, `date_taken_to`
  - `sort_by`, `sort_dir`
  - `tag_names`, `match_all`
  - **❌ NO `photo_id` filter currently supported**

### Backend API (`src/web/api/faces.py`)
- **Endpoint**: `GET /api/v1/faces/unidentified` (lines 104-171)
- **Service Function**: `list_unidentified_faces()` in `face_service.py` (lines 1194-1300)
- **Current Filters**: Quality, dates, tags
- **❌ NO `photo_id` parameter in service function**

### Routing (`frontend/src/App.tsx`)
- Uses React Router v6
- Identify route: `/identify` (line 42)
- Can use `useNavigate()` hook for navigation
- Can pass state via `navigate('/identify', { state: {...} })`
- Can use URL search params: `/identify?photo_ids=1,2,3`
- Can open in new tab: `window.open('/identify?photo_ids=1,2,3', '_blank')`

## What's Needed

1. **Get faces for selected photo(s)**
   - Need API endpoint or modify existing to filter by `photo_id`
   - Only get faces if photo is processed (`photo.processed === true`)
   - Only get unidentified faces (no `person_id`)

2. **Navigate to Identify page**
   - Pass face IDs or photo IDs to Identify page
   - Load only those faces in the left panel (reference faces)
   - Optionally open in new tab

3. **Identify page modifications**
   - Check for photo_ids or face_ids in URL params or state
   - If provided, load only those faces instead of all unidentified faces
   - Display them in the left panel as reference faces

## Possible Approaches

### Approach A: Add `photo_id` filter to existing `/unidentified` endpoint
**Pros:**
- Minimal changes to existing API
- Reuses existing filtering logic
- Consistent with other filters

**Cons:**
- Only works for unidentified faces
- Need to support multiple photo_ids (array)

**Implementation:**
1. Add `photo_ids: Optional[List[int]]` parameter to `list_unidentified_faces()` service
2. Add `photo_ids: Optional[str]` query param to API endpoint (comma-separated)
3. Filter query: `query.filter(Face.photo_id.in_(photo_ids))`
4. Frontend: Pass `photo_ids` in `getUnidentified()` call
5. Identify page: Check URL params for `photo_ids`, parse and pass to API

### Approach B: Create new endpoint `/api/v1/faces/by-photo/{photo_id}`
**Pros:**
- Clean separation of concerns
- Can return all faces (identified + unidentified) if needed later
- More explicit purpose

**Cons:**
- New endpoint to maintain
- Need to handle multiple photos (could use POST with array)

**Implementation:**
1. Create `GET /api/v1/faces/by-photo/{photo_id}` endpoint
2. Or `POST /api/v1/faces/by-photos` with `{photo_ids: [1,2,3]}`
3. Return `UnidentifiedFacesResponse` format
4. Frontend: Call new endpoint from Tags page
5. Navigate with face IDs in state/URL params

### Approach C: Use URL params to pass photo_ids, filter on frontend
**Pros:**
- No backend changes needed
- Quick to implement

**Cons:**
- Need to load ALL unidentified faces first, then filter
- Inefficient for large databases
- Not scalable

**Implementation:**
1. Tags page: Navigate to `/identify?photo_ids=1,2,3`
2. Identify page: Load all unidentified faces
3. Filter faces array: `faces.filter(f => photoIds.includes(f.photo_id))`
4. ❌ **Not recommended** - inefficient

## Recommended Solution: Approach A (Extend Existing Endpoint)

### Why Approach A?
- Minimal backend changes
- Efficient (database-level filtering)
- Consistent with existing API patterns
- Supports multiple photos easily

### Implementation Plan

#### 1. Backend Changes

**File: `src/web/services/face_service.py`**
```python
def list_unidentified_faces(
    db: Session,
    page: int = 1,
    page_size: int = 50,
    min_quality: float = 0.0,
    date_from: Optional[date] = None,
    date_to: Optional[date] = None,
    date_taken_from: Optional[date] = None,
    date_taken_to: Optional[date] = None,
    date_processed_from: Optional[date] = None,
    date_processed_to: Optional[date] = None,
    sort_by: str = "quality",
    sort_dir: str = "desc",
    tag_names: Optional[List[str]] = None,
    match_all: bool = False,
    photo_ids: Optional[List[int]] = None,  # NEW PARAMETER
) -> Tuple[List[Face], int]:
    # ... existing code ...
    
    # Photo ID filtering (NEW)
    if photo_ids:
        query = query.filter(Face.photo_id.in_(photo_ids))
    
    # ... rest of existing code ...
```

**File: `src/web/api/faces.py`**
```python
@router.get("/unidentified", response_model=UnidentifiedFacesResponse)
def get_unidentified_faces(
    # ... existing params ...
    photo_ids: str | None = Query(None, description="Comma-separated photo IDs"),
    db: Session = Depends(get_db),
) -> UnidentifiedFacesResponse:
    # ... existing code ...
    
    # Parse photo_ids
    photo_ids_list = None
    if photo_ids:
        try:
            photo_ids_list = [int(pid.strip()) for pid in photo_ids.split(',') if pid.strip()]
        except ValueError:
            raise HTTPException(status_code=400, detail="Invalid photo_ids format")
    
    faces, total = list_unidentified_faces(
        # ... existing params ...
        photo_ids=photo_ids_list,  # NEW PARAMETER
    )
    # ... rest of existing code ...
```

**File: `frontend/src/api/faces.ts`**
```typescript
getUnidentified: async (params: {
  // ... existing params ...
  photo_ids?: string,  // NEW: comma-separated photo IDs
}): Promise<UnidentifiedFacesResponse> => {
  // ... existing code ...
}
```

#### 2. Frontend Changes

**File: `frontend/src/pages/Tags.tsx`**
Add button/action to selected photos:
```typescript
// Add state for "Identify Faces" action
const handleIdentifyFaces = (photoIds: number[]) => {
  // Filter to only processed photos with faces
  const processedPhotos = photos.filter(p => 
    photoIds.includes(p.id) && p.processed && p.face_count > 0
  )
  
  if (processedPhotos.length === 0) {
    alert('No processed photos with faces selected')
    return
  }
  
  // Navigate to Identify page with photo IDs
  const photoIdsStr = processedPhotos.map(p => p.id).join(',')
  
  // Option 1: Same tab
  navigate(`/identify?photo_ids=${photoIdsStr}`)
  
  // Option 2: New tab
  // window.open(`/identify?photo_ids=${photoIdsStr}`, '_blank')
}
```

**File: `frontend/src/pages/Identify.tsx`**
Modify to check for `photo_ids` URL param:
```typescript
import { useSearchParams } from 'react-router-dom'

export default function Identify() {
  const [searchParams] = useSearchParams()
  const photoIdsParam = searchParams.get('photo_ids')
  
  // Parse photo IDs from URL
  const photoIds = useMemo(() => {
    if (!photoIdsParam) return null
    return photoIdsParam.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
  }, [photoIdsParam])
  
  const loadFaces = async (clearState: boolean = false) => {
    setLoadingFaces(true)
    
    try {
      const res = await facesApi.getUnidentified({
        page: 1,
        page_size: pageSize,
        min_quality: minQuality,
        date_taken_from: dateFrom || undefined,
        date_taken_to: dateTo || undefined,
        sort_by: sortBy,
        sort_dir: sortDir,
        tag_names: selectedTags.length > 0 ? selectedTags.join(', ') : undefined,
        match_all: false,
        photo_ids: photoIds ? photoIds.join(',') : undefined,  // NEW
      })
      
      // ... rest of existing code ...
    } finally {
      setLoadingFaces(false)
    }
  }
  
  // ... rest of component ...
}
```

#### 3. UI Enhancement in Tags Page

Add a button/action when photos are selected:
```tsx
{selectedPhotoIds.size > 0 && (
  <button
    onClick={() => {
      const photoIds = Array.from(selectedPhotoIds)
      handleIdentifyFaces(photoIds)
    }}
    className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
  >
    🔍 Identify Faces ({selectedPhotoIds.size} photo{selectedPhotoIds.size !== 1 ? 's' : ''})
  </button>
)}
```

Or add a context menu/button on individual photos:
```tsx
{photo.processed && photo.face_count > 0 && (
  <button
    onClick={(e) => {
      e.stopPropagation()
      handleIdentifyFaces([photo.id])
    }}
    className="px-2 py-1 bg-indigo-600 text-white rounded text-xs hover:bg-indigo-700"
    title="Identify faces in this photo"
  >
    🔍 Identify
  </button>
)}
```

## Implementation Considerations

### 1. **Photo Processing Status**
- Only show action for processed photos (`photo.processed === true`)
- Only show if `photo.face_count > 0`
- Show appropriate message if no processed photos selected

### 2. **New Tab vs Same Tab**
- **Same Tab**: User loses Tag page context, but simpler navigation
- **New Tab**: Preserves Tag page, better UX for comparison
- **Recommendation**: Start with same tab, add option for new tab later

### 3. **Multiple Photos**
- Support multiple photo selection
- Combine all faces from selected photos
- Show count: "X faces from Y photos"

### 4. **Empty Results**
- If no faces found for selected photos, show message
- Could be because:
  - Photos not processed yet
  - All faces already identified
  - No faces detected

### 5. **URL Parameter Length**
- For many photos, URL could get long
- Consider using POST with state instead of URL params
- Or use sessionStorage to pass photo IDs

### 6. **State Management**
- Identify page uses sessionStorage for state persistence
- Need to handle case where photo_ids override normal loading
- Clear photo_ids filter when user clicks "Refresh" or "Apply Filters"

## Alternative: Using State Instead of URL Params

If URL params become too long or we want to avoid exposing photo IDs:

```typescript
// Tags page
navigate('/identify', { 
  state: { 
    photoIds: processedPhotos.map(p => p.id),
    source: 'tags' 
  } 
})

// Identify page
const location = useLocation()
const photoIds = location.state?.photoIds

// But this doesn't work for new tabs - would need sessionStorage
```

## Testing Checklist

- [ ] Select single processed photo with faces → Navigate to Identify
- [ ] Select multiple processed photos → Navigate to Identify
- [ ] Select unprocessed photo → Show appropriate message
- [ ] Select photo with no faces → Show appropriate message
- [ ] Select mix of processed/unprocessed → Only process processed ones
- [ ] Navigate with photo_ids → Only those faces shown
- [ ] Clear filters in Identify → Should clear photo_ids filter
- [ ] Refresh in Identify → Should maintain photo_ids filter (or clear?)
- [ ] Open in new tab → Works correctly
- [ ] URL with many photo_ids → Handles correctly

## Summary

**Feasibility**: ✅ **YES, this is possible**

**Recommended Approach**: Extend existing `/unidentified` endpoint with `photo_ids` filter

**Key Changes Needed**:
1. Backend: Add `photo_ids` parameter to service and API
2. Frontend: Add navigation from Tags to Identify with photo_ids
3. Frontend: Modify Identify page to handle photo_ids URL param
4. UI: Add button/action in Tags page for selected photos

**Complexity**: Low-Medium
- Backend: Simple filter addition
- Frontend: URL param handling + navigation
- UI: Button/action addition

**Estimated Effort**: 2-4 hours





