diff --git a/src/views/PeopleView.vue b/src/views/PeopleView.vue index bb3e632..8c748c6 100644 --- a/src/views/PeopleView.vue +++ b/src/views/PeopleView.vue @@ -1097,7 +1097,14 @@ const faceMoveTarget = ref('') const faceMoveNewGroupName = ref('') // Cluster results (synced with store for cross-navigation persistence) -const clusterResults = computed(() => clusterResultsCache.value[selectedFileUuid.value] || []) +const clusterResults = computed(() => { + const results = clusterResultsCache.value[selectedFileUuid.value] || [] + console.log('[clusterResults] cache for', selectedFileUuid.value, ':', results.length, 'groups') + if (results.length > 0) { + console.log('[clusterResults] first group:', JSON.stringify(results[0], null, 2)) + } + return results +}) const clusterTracesMap = ref>({}) const clusterRunning = computed(() => isClusterRunning(selectedFileUuid.value)) @@ -1131,6 +1138,10 @@ const faceGroups = computed(() => { } const defaultName = `group ${String(idx + 1).padStart(2, '0')}` if (!c.name) c.name = defaultName + // Convert Person_X to Group_X for display + if (c.name && c.name.startsWith('Person_')) { + c.name = 'Group_' + c.name.substring(7) + } const visibleTraceIds = ffFilterActive.value ? traceIds.filter(tid => !ffHiddenTraces.value.has(tid)) : traceIds @@ -1176,11 +1187,19 @@ const clusterDetailCluster = computed(() => { // Map trace_id → group name for Faces section const traceGroupMap = computed(() => { const map: Record = {} + console.log('[traceGroupMap] clusterResults:', clusterResults.value.length, 'groups') for (const c of clusterResults.value) { + let name = c.name || '' + // Convert Person_X to Group_X for display + if (name.startsWith('Person_')) { + name = 'Group_' + name.substring(7) + } + console.log('[traceGroupMap] cluster:', c.cluster_id, 'name:', name, 'trace_ids:', c.trace_ids) for (const tid of c.trace_ids || []) { - map[String(tid)] = c.name + map[String(tid)] = name } } + console.log('[traceGroupMap] final map:', map) return map }) @@ -1218,7 +1237,25 @@ const facesTotalPages = computed(() => { }) function traceLabel(tr: any): string { - const n = tr?.name || traceGroupMap.value[String(tr.trace_id)] + // Prefer cluster name from clusterResultsCache + const clusters = clusterResultsCache.value[selectedFileUuid.value] || [] + console.log('[traceLabel] trace_id:', tr.trace_id, 'clusters in cache:', clusters.length) + if (clusters.length > 0) { + console.log('[traceLabel] first cluster:', clusters[0]) + } + for (const c of clusters) { + if (c.trace_ids && c.trace_ids.includes(tr.trace_id)) { + let name = c.name || '' + // Convert Person_X to Group_X for display + if (name.startsWith('Person_')) { + name = 'Group_' + name.substring(7) + } + console.log('[traceLabel] Found cluster name:', name) + return name + } + } + const n = tr?.name + console.log('[traceLabel] Fallback to:', n || t('people.section.unassigned')) return n || t('people.section.unassigned') } @@ -2028,6 +2065,11 @@ async function loadClusterResults() { } const groups = results?.face_groups || results?.clusters || [] const frames = results?.frames || [] + console.log('[loadClusterResults] groups:', groups.length, 'frames:', frames.length) + if (frames.length > 0) { + console.log('[loadClusterResults] first frame faces:', frames[0].faces) + console.log('[loadClusterResults] first group:', groups[0]) + } // 統一 group_id → cluster_id, 並添加 group_uuid for (const g of groups) { if (g.group_id !== undefined && g.cluster_id === undefined) { @@ -2069,6 +2111,7 @@ async function loadClusterResults() { const cid = c.cluster_id || c.group_id c.member_faces = clusterFacesMap[cid] || [] c.face_count = c.member_faces.length + console.log(`[loadClusterResults] group ${c.name} (cid=${cid}): member_faces=${c.member_faces.length}`) c.trace_ids = Array.from(clusterTraceIdsMap[cid] || []) // Set representative_trace from representative_face if (c.representative_face?.face_id) { @@ -2083,7 +2126,10 @@ async function loadClusterResults() { } } if (groups.length) { + console.log('[loadClusterResults] Calling setClusterResults with', groups.length, 'groups') + console.log('[loadClusterResults] First group trace_ids:', groups[0]?.trace_ids) setClusterResults(fileUuid, groups) + console.log('[loadClusterResults] After setClusterResults, cache:', clusterResultsCache.value[fileUuid]?.length) // Load trace profiles for names in background (non-blocking) loadClusterNames(fileUuid, groups).catch(() => {}) @@ -2749,7 +2795,7 @@ function closeClusterCtxMenu(e?: MouseEvent) { clusterCtxMenu.value.show = false } -function clusterCtxAction(action: string) { +async function clusterCtxAction(action: string) { const c = clusterCtxMenu.value.cluster if (!c) return clusterCtxMenu.value.show = false @@ -2766,7 +2812,7 @@ function clusterCtxAction(action: string) { // Update trace profiles via API const traceIds = clusters[idx]?.trace_ids || [] if (traceIds.length && selectedFileUuid.value) { - updateTraceProfileGroup(selectedFileUuid.value, traceIds, { name }) + await updateTraceProfileGroup(selectedFileUuid.value, traceIds, { name }) } } } else if (action === 'skip') { @@ -3297,24 +3343,75 @@ async function executeMerge(targetGroupId: string) { const sourceGroupIds = groupIds.filter(id => id !== targetGroupId) const sourceGroups = faceGroups.value.filter((c: any) => sourceGroupIds.includes(c.group_uuid)) - const sourceGroupNames = sourceGroups.map(g => g.name).filter(Boolean) + // Collect all trace_ids from source groups + const allTraceIds: number[] = [] + for (const g of sourceGroups) { + if (g.trace_ids) { + allTraceIds.push(...g.trace_ids) + } + } - if (sourceGroupNames.length === 0) { - alert('No valid source groups to merge') + if (allTraceIds.length === 0) { + alert('No traces to merge') return } - if (!confirm(`Merge ${sourceGroupNames.length} group(s) into "${targetGroupName}"?`)) return + if (!confirm(`Merge ${sourceGroups.length} group(s) with ${allTraceIds.length} trace(s) into "${targetGroupName}"?`)) return operationLoading.value = true - operationMessage.value = `Merging ${sourceGroupNames.length} group(s)...` + operationMessage.value = `Merging ${sourceGroups.length} group(s)...` try { - const result = await apiCall('merge_groups', { - fileUuid: selectedFileUuid.value, - sourceGroups: sourceGroupNames, - targetGroupName: targetGroupName - }) + // Update all source traces to target group name + console.log('[executeMerge] Updating trace_ids:', allTraceIds, 'to name:', targetGroupName) + + // 1. Update trace_profiles via API + const result = await updateTraceProfileGroup(selectedFileUuid.value, allTraceIds, { name: targetGroupName }) + console.log('[executeMerge] API Result:', result) + + // 2. Update clusterResultsCache locally + const clusters = getClusterResults(selectedFileUuid.value) + + // Find target cluster + const targetCluster = clusters.find((c: any) => c.name === targetGroupName || c.cluster_id === targetGroup.cluster_id) + + if (targetCluster) { + // Add source trace_ids to target cluster (avoid duplicates) + if (!targetCluster.trace_ids) targetCluster.trace_ids = [] + for (const tid of allTraceIds) { + if (!targetCluster.trace_ids.includes(tid)) { + targetCluster.trace_ids.push(tid) + } + } + targetCluster.trace_count = targetCluster.trace_ids.length + } + + // Remove trace_ids from source clusters + for (const sourceGroup of sourceGroups) { + const sourceCluster = clusters.find((c: any) => c.cluster_id === sourceGroup.cluster_id || c.group_id === sourceGroup.group_id) + if (sourceCluster && sourceCluster.trace_ids) { + sourceCluster.trace_ids = sourceCluster.trace_ids.filter((tid: number) => !allTraceIds.includes(tid)) + sourceCluster.trace_count = sourceCluster.trace_ids.length + } + } + + // Remove empty source clusters + const nonEmptyClusters = clusters.filter((c: any) => !sourceGroups.some(sg => + (c.cluster_id === sg.cluster_id || c.group_id === sg.group_id) && (!c.trace_ids || c.trace_ids.length === 0) + )) + + setClusterResults(selectedFileUuid.value, nonEmptyClusters) + console.log('[executeMerge] Updated cache') + + // Reload thumbnails for updated clusters + for (const c of nonEmptyClusters) { + if (c.trace_ids && c.trace_ids.length > 0) { + const repId = c.representative_trace || c.trace_ids[0] + if (repId) { + loadTraceThumb({ trace_id: repId, file_uuid: selectedFileUuid.value, best_face_id: 0 }) + } + } + } if (result?.success) { @@ -4121,7 +4218,7 @@ h1 { margin: 0; } /* Face detail side navigation arrows */ .ms-fs-nav-arrow { - position: fixed; top: 50%; transform: translateY(-50%); z-index: 10010; + position: fixed; top: 50%; transform: translateY(-50%); z-index: 10100; width: 44px; height: 44px; border-radius: 50%; border: none; background: rgba(0,0,0,0.5); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center;