Compare commits

...

30 Commits

Author SHA1 Message Date
2b944cf9d1 fix: use search/people API (Core Team deployed)
- Remove duplicate search_people case
- Remove search_identities (no longer needed)
- Add search_people response handler
- SearchView already uses search_people
2026-07-25 23:16:59 +08:00
453168f326 chore: remove unused .gs-sidebar-lock CSS 2026-07-25 21:13:22 +08:00
bcffe62a15 fix: remove lock icon from sidebar toggle 2026-07-25 21:13:07 +08:00
ee9a602f7f fix: toggle shows expand chevron when locked, lock icon when unlocked 2026-07-25 20:57:58 +08:00
3aef7d115f fix: sidebar default to collapsed 2026-07-25 20:56:29 +08:00
19846a8773 revert: restore localStorage-based sidebar default 2026-07-25 20:51:48 +08:00
bb4ff9b007 fix: sidebar default to collapsed (locked) 2026-07-25 20:51:38 +08:00
fcb612ff0e revert: restore search_identities API case 2026-07-25 20:38:54 +08:00
1a0b3f2c1f fix: use search/people API (no identity concept) 2026-07-25 20:38:30 +08:00
9386a8e8ef fix: fallback to search_identities (search/people endpoint not deployed) 2026-07-25 20:35:47 +08:00
fb9a55a64c feat: use new search/people API for People search mode 2026-07-25 20:33:15 +08:00
9691e75e36 fix: use height:100% on sidebar/main (resolves to parent content height) 2026-07-25 20:29:35 +08:00
674126d687 fix: add html/body height:100% + 100% height on sidebar/main
- html,body { height:100% } to ensure proper height chain
- ms-side { height: 100% } and ms-main { height: 100% }
- Both resolve to same parent content height: 100vh - 56px
2026-07-25 20:29:26 +08:00
d37967d38e fix: explicit calc(100vh - 56px) on both sidebar and main 2026-07-25 20:28:56 +08:00
a289346fdc fix: revert to flexbox with height:100vh, fix footer visibility
- height:100vh (not min-height) so align-items:stretch actually works
- Footer shows when expanded, hides when collapsed (dynamic class)
- Remove align-self:stretch (unnecessary with flex stretch)
- Restore flex-shrink on divider
2026-07-25 20:27:09 +08:00
dd843c29ee fix: use CSS grid for equal-height sidebar and main 2026-07-25 20:24:11 +08:00
d7592c528c fix: sync sidebar height with main content area 2026-07-25 20:21:17 +08:00
f75eb44351 feat: show compact login/logout icon when sidebar collapsed 2026-07-25 20:20:03 +08:00
652ba77c77 fix: use CSS instead of v-show for sidebar collapse text
- Replace v-show with CSS .gs-nav-label + .collapsed display: none
- Use CSS class .gs-hidden instead of v-show for footer/divider
- Fix content not re-displaying after collapse/expand
2026-07-25 20:18:03 +08:00
ca94259fba feat: add sidebar collapse/expand toggle 2026-07-25 20:11:56 +08:00
660f01c6c5 fix: People search mode label 2026-07-25 19:27:56 +08:00
cb7ebfb633 feat: add Face Group search mode back to SearchView
- Add 'Face Group' mode to search modes
- Use search_identities API temporarily
- Update summary: 'Identity' → 'FS'
- Will replace with Core Team's search_tkg_node_face_trace_profile API
2026-07-25 19:24:54 +08:00
25f718c0d5 fix: remove duplicate case clauses in api/index.ts 2026-07-25 18:28:04 +08:00
0b19a0af0d refactor: QC report - 'Trace Profile' → 'FS Profile' 2026-07-25 15:53:37 +08:00
cf397b268e refactor: QC - use FS instead of Trace
- Display format: FS#123 instead of Trace #123
- Update labels: 'Traces' → 'FS'
- Update messages: 'clusters' → 'groups'
2026-07-25 15:53:17 +08:00
aa25600580 refactor: remove identity/pending concepts
- identity_uuid → group_uuid
- Remove status: 'pending'
- Remove starred: false
- Keep only: face, FS, face group
2026-07-25 15:51:38 +08:00
464ddd16a9 chore: remove debug console.log from faceGroups 2026-07-25 15:49:27 +08:00
4463ff2f96 style: change 'Faces' → 'FS' in Face Groups card 2026-07-25 15:48:13 +08:00
15647a47ac fix: replace all clusterAsPending → faceGroups 2026-07-25 15:43:28 +08:00
0a7902c2f5 refactor: rename clusterAsPending → faceGroups (no pending concept)
- Rename computed property to match current API semantics
- face_groups are definitive, not pending
- Add detailed console.log for debugging empty groups issue
2026-07-25 15:43:15 +08:00
5 changed files with 96 additions and 65 deletions

Binary file not shown.

View File

@@ -15,6 +15,12 @@ const currentLanguage = ref(getCurrentLanguage())
const sidebarWidth = ref(240)
const isDragging = ref(false)
const sidebarCollapsed = ref(true)
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem('sidebar_collapsed', String(sidebarCollapsed.value))
}
function refreshUser() {
const user = localStorage.getItem('username') || ''
@@ -98,15 +104,21 @@ onUnmounted(() => {
<template>
<BuildVersion />
<div id="app" class="ms-app" :data-current-login="'guest'">
<aside class="ms-side" :style="{ width: sidebarWidth + 'px', minWidth: sidebarWidth + 'px' }">
<div class="gs-logo">Workspace</div>
<aside class="ms-side" :class="{ collapsed: sidebarCollapsed }" :style="{ width: sidebarCollapsed ? '62px' : sidebarWidth + 'px', minWidth: sidebarCollapsed ? '62px' : sidebarWidth + 'px' }">
<div class="gs-sidebar-toggle" @click="toggleSidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
:style="{ transform: sidebarCollapsed ? 'rotate(0deg)' : 'rotate(180deg)' }">
<polyline points="9 18 15 12 9 6"/>
</svg>
</div>
<div class="gs-logo" :class="{ 'gs-logo-collapsed': sidebarCollapsed }">{{ sidebarCollapsed ? 'WS' : 'Workspace' }}</div>
<nav class="gs-nav">
<router-link to="/search" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<span>{{ t('nav.search') }}</span>
<span class="gs-nav-label">{{ t('nav.search') }}</span>
</router-link>
<router-link to="/library" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -115,7 +127,7 @@ onUnmounted(() => {
<rect x="3" y="14" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
<span>{{ t('nav.library') }}</span>
<span class="gs-nav-label">{{ t('nav.library') }}</span>
</router-link>
<router-link to="/face" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -123,24 +135,24 @@ onUnmounted(() => {
<circle cx="12" cy="10" r="4"/>
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
</svg>
<span>Face</span>
<span class="gs-nav-label">Face</span>
</router-link>
<router-link to="/admin" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span>{{ t('nav.admin') }}</span>
<span class="gs-nav-label">{{ t('nav.admin') }}</span>
</router-link>
<router-link to="/client" class="gs-nav-item" active-class="active">
<svg class="gs-nav-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span>{{ t('nav.client') }}</span>
<span class="gs-nav-label">{{ t('nav.client') }}</span>
</router-link>
</nav>
<div class="gs-divider"></div>
<div class="gs-footer">
<div class="gs-divider" :class="{ 'gs-hidden': sidebarCollapsed }"></div>
<div class="gs-footer" :class="{ 'gs-hidden': sidebarCollapsed }">
<div class="gs-lang-switcher">
<select v-model="currentLanguage" @change="onChangeLanguage" class="gs-lang-select">
<option value="en">English</option>
@@ -158,8 +170,16 @@ onUnmounted(() => {
</template>
</div>
</div>
<div class="gs-footer-compact">
<template v-if="loggedInUser">
<button class="gs-logout-btn-compact" @click="handleLogout" title="Logout"></button>
</template>
<template v-else>
<router-link to="/login" class="gs-login-link-compact" title="Login"></router-link>
</template>
</div>
</aside>
<div class="ms-divider" @mousedown="startDrag"></div>
<div class="ms-divider" :class="{ 'ms-divider-hidden': sidebarCollapsed }" @mousedown="startDrag"></div>
<main class="ms-main">
<router-view />
</main>
@@ -169,13 +189,30 @@ onUnmounted(() => {
<style>
@import './assets/wordpress-exact.css';
html, body { height: 100%; margin: 0; }
</style>
<style scoped>
* { margin: 0; padding: 0; box-sizing: border-box; }
#app, .ms-app { background: var(--background-color) !important; display: flex; max-width: 1400px; margin: 0 auto; padding: 28px; gap: 0; min-height: 100vh; box-sizing: border-box; align-items: stretch; }
.ms-side { width: 240px; min-width: 240px; flex-shrink: 0; background: var(--card-background) !important; border-radius: 18px; padding: 18px 14px; box-shadow: var(--shadow); display: flex; flex-direction: column; position: relative; height: calc(100vh - 125px); align-self: flex-start; overflow: visible; }
#app, .ms-app { background: var(--background-color) !important; display: flex; max-width: 1400px; margin: 0 auto; padding: 28px; gap: 0; height: 100vh; box-sizing: border-box; }
.ms-side { width: 240px; min-width: 240px; background: var(--card-background) !important; border-radius: 18px; padding: 18px 14px; box-shadow: var(--shadow); display: flex; flex-direction: column; position: relative; overflow: visible; height: 100%; }
.ms-side.collapsed { align-items: center; padding: 18px 8px; }
.ms-side.collapsed .gs-nav { align-items: center; width: 100%; }
.ms-side.collapsed .gs-nav-item { justify-content: center; padding: 0; width: 44px; }
.ms-side.collapsed .gs-nav-label { display: none; }
.ms-side.collapsed .gs-nav-icon { width: 22px; height: 22px; }
.gs-footer-compact { display: none; }
.ms-side.collapsed .gs-footer-compact { display: flex; flex-direction: column; align-items: center; margin-top: auto; padding: 10px 0; }
.gs-logout-btn-compact, .gs-login-link-compact { background: none; border: 1px solid var(--border-color); cursor: pointer; font-size: 16px; color: var(--text-secondary); width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; text-decoration: none; transition: all .15s; }
.gs-logout-btn-compact:hover { color: var(--danger-color); border-color: var(--danger-color); background: var(--danger-background); }
.gs-login-link-compact:hover { color: var(--text-primary); border-color: var(--text-primary); }
.gs-hidden { display: none !important; }
.gs-sidebar-toggle { display: flex; align-items: center; justify-content: flex-end; padding: 0 4px 12px 4px; cursor: pointer; color: var(--text-secondary); flex-shrink: 0; }
.gs-sidebar-toggle svg { transition: transform 0.2s; }
.ms-side.collapsed .gs-sidebar-toggle { justify-content: center; width: 100%; }
.gs-logo-collapsed { text-align: center; padding: 8px 4px 16px 4px; font-size: 14px; }
.ms-divider { width: 28px; flex-shrink: 0; cursor: col-resize; display: flex; align-items: center; justify-content: center; }
.ms-divider-hidden { cursor: default; pointer-events: none; opacity: 0; }
.ms-divider::after { content: ''; width: 4px; height: 40px; background: var(--border-color); border-radius: 2px; transition: background 0.2s, height 0.2s; }
.ms-divider:hover::after { background: var(--primary-color); height: 60px; }
.gs-logo { font-size: 15px; font-weight: 600; color: var(--text-primary); padding: 8px 12px 16px 12px; flex-shrink: 0; }
@@ -195,6 +232,6 @@ onUnmounted(() => {
.gs-logout-btn:hover { color: var(--danger-color); border-color: var(--danger-color); background: var(--danger-background); }
.gs-login-link { font-size: 12px; color: var(--text-secondary); text-decoration: none; }
.gs-login-link:hover { color: var(--text-primary); }
.ms-main { flex: 1; min-width: 0; background: var(--card-background) !important; border-radius: 18px; padding: 36px 42px; overflow-y: auto; overflow-x: hidden; }
.ms-main { flex: 1; min-width: 0; background: var(--card-background) !important; border-radius: 18px; padding: 36px 42px; overflow-y: auto; overflow-x: hidden; height: 100%; }
.ms-content { max-width: 1200px; }
</style>

View File

@@ -255,14 +255,18 @@ case 'search_keyword': {
case 'search_llm_smart': {
return { url: '/api/v1/search/llm-smart', method: 'POST', body: { query: a.query, limit: a.limit || 20 } }
}
case 'search_people': {
const params = new URLSearchParams()
if (a.query) params.set('query', a.query)
if (a.fileUuid) params.set('file_uuid', a.fileUuid)
if (a.limit) params.set('limit', String(a.limit))
return { url: `/api/v1/search/people?${params.toString()}`, method: 'GET' }
}
case 'search_agents': {
const body: any = { query: a.query }
if (a.conversationId) body.conversation_id = a.conversationId
return { url: '/api/v1/agents/search', method: 'POST', body }
}
case 'search_identities': {
return { url: `/api/v1/identities/search?q=${encodeURIComponent(a.query)}&limit=${a.limit || 50}`, method: 'GET' }
}
// --- Image APIs ---
case 'get_thumbnail': {
@@ -471,12 +475,6 @@ case 'search_keyword': {
case 'get_cluster_results': {
return { url: `/api/v1/cluster-results?file_hash=${a.fileHash}`, method: 'GET' }
}
case 'get_trace_profile': {
return { url: `/api/v1/trace-profile?file_uuid=${a.fileUuid}&trace_id=${a.traceId}`, method: 'GET' }
}
case 'update_trace_profile': {
return { url: '/api/v1/trace-profile', method: 'PUT', body: { file_uuid: a.fileUuid, trace_id: a.traceId, label: a.label, key_frame: a.keyFrame, key_face: a.keyFace, aliases: a.aliases } }
}
case 'update_trace_profile_group': {
return { url: '/api/v1/trace-profile/group', method: 'PUT', body: { file_uuid: a.fileUuid, trace_ids: a.traceIds, name: a.name, key_frame: a.keyFrame, key_face: a.keyFace } }
}
@@ -766,6 +764,9 @@ case 'get_unassigned_traces': {
source_type: r.source_type || null,
}))
}
case 'search_people': {
return data || { people: [] }
}
case 'search_identities': {
const results = data.results || data.data || data || []
return results.map((r: any) => ({

View File

@@ -39,9 +39,9 @@
</div>
<template v-else>
<!-- Face Group -->
<div v-if="showCluster && clusterAsPending.length" class="ms-ppl-section">
<div v-if="showCluster && faceGroups.length" class="ms-ppl-section">
<div class="ms-ppl-section-toolbar">
<div class="ms-ppl-section-title">{{ t('people.section.face_group') }} <span class="ms-ppl-section-count">({{ clusterAsPending.length }})</span></div>
<div class="ms-ppl-section-title">{{ t('people.section.face_group') }} <span class="ms-ppl-section-count">({{ faceGroups.length }})</span></div>
<button v-if="!batchMode" class="ms-fm-btn ms-fm-btn-sm" @click="batchMode = true"> Select</button>
<template v-else>
<button class="ms-fm-btn ms-fm-btn-sm ms-fm-btn-primary" @click="batchMergeGroups" :disabled="selectedGroups.length < 2"> Merge ({{ selectedGroups.length }})</button>
@@ -50,7 +50,7 @@
</template>
</div>
<div class="ms-ppl-face-grid">
<div v-for="c in clusterAsPending" :key="c.identity_uuid" class="ms-ppl-face-card ms-ppl-cluster-card" :class="{ 'ms-ppl-face-card-selected': isGroupSelected(c) }" @click="batchMode ? toggleGroupSelection(c) : selectCluster(c)" title="View member faces">
<div v-for="c in faceGroups" :key="c.group_uuid" class="ms-ppl-face-card ms-ppl-cluster-card" :class="{ 'ms-ppl-face-card-selected': isGroupSelected(c) }" @click="batchMode ? toggleGroupSelection(c) : selectCluster(c)" title="View member faces">
<div v-if="batchMode" class="ms-ppl-card-checkbox" @click.stop="toggleGroupSelection(c)">
<span v-if="isGroupSelected(c)"></span>
</div>
@@ -62,7 +62,7 @@
</svg>
<span class="ms-ppl-cluster-name-overlay">{{ c.name }}</span>
</div>
<span class="ms-ppl-face-name">{{ c.trace_count }} Faces</span>
<span class="ms-ppl-face-name">{{ c.trace_count }} FS</span>
</div>
</div>
</div>
@@ -283,7 +283,7 @@
<h2 class="ms-ppl-section-title">Select Target Group</h2>
<p class="ms-merge-hint">Choose which group to keep. Other selected groups will be merged into this one.</p>
<div class="ms-merge-grid">
<div v-for="c in mergeCandidateGroups" :key="c.identity_uuid" class="ms-merge-face-card" @click="executeMerge(c.identity_uuid)">
<div v-for="c in mergeCandidateGroups" :key="c.group_uuid" class="ms-merge-face-card" @click="executeMerge(c.group_uuid)">
<div class="ms-merge-face-img">
<img v-if="c.rep_trace && faceThumbsCache[thumbTraceKey(c.rep_trace)]" :src="faceThumbsCache[thumbTraceKey(c.rep_trace)]" alt="">
<svg v-else viewBox="0 0 120 120" fill="none" style="width:100%;height:100%;">
@@ -313,7 +313,7 @@
<hr class="ms-merge-divider">
<div class="ms-merge-grid">
<div v-for="c in clusterAsPending" :key="c.identity_uuid" class="ms-merge-face-card" @click="moveToGroup(c)">
<div v-for="c in faceGroups" :key="c.group_uuid" class="ms-merge-face-card" @click="moveToGroup(c)">
<div class="ms-merge-face-img">
<img v-if="c.rep_trace && faceThumbsCache[thumbTraceKey(c.rep_trace)]" :src="faceThumbsCache[thumbTraceKey(c.rep_trace)]" alt="">
<svg v-else viewBox="0 0 120 120" fill="none" style="width:100%;height:100%;">
@@ -394,7 +394,7 @@
<div class="ms-qc-stats">
<div class="ms-qc-stat">
<span class="ms-qc-stat-value">{{ qcProgress.total }}</span>
<span class="ms-qc-stat-label">Traces</span>
<span class="ms-qc-stat-label">FS</span>
</div>
<div class="ms-qc-stat">
<span class="ms-qc-stat-value">{{ qcProgress.current }}</span>
@@ -443,7 +443,7 @@
<h3> Profile Issues ({{ qcProfileIssues.length }})</h3>
<div class="ms-qc-issues-list">
<div v-for="(p, i) in qcProfileIssues.slice(0, 10)" :key="i" class="ms-qc-issue-item">
<span class="ms-qc-issue-trace">Trace #{{ p.traceId }}</span>
<span class="ms-qc-issue-trace">FS#{{ p.traceId }}</span>
<span class="ms-qc-issue-group">{{ p.groupName }}</span>
<span class="ms-qc-issue-status" :class="{ fail: !p.canRead }">{{ p.canRead ? '✓ R' : '✗ R' }}</span>
<span class="ms-qc-issue-status" :class="{ fail: !p.canWrite }">{{ p.canWrite ? '✓ W' : '✗ W' }}</span>
@@ -537,7 +537,7 @@ function isFaceSelected(t: any): boolean {
}
function isGroupSelected(c: any): boolean {
return selectedGroups.value.includes(c.identity_uuid)
return selectedGroups.value.includes(c.group_uuid)
}
function isClusterTraceSelected(t: any): boolean {
@@ -631,16 +631,10 @@ const clusterResults = computed(() => clusterResultsCache.value[selectedFileUuid
const clusterTracesMap = ref<Record<string, any>>({})
const clusterRunning = computed(() => isClusterRunning(selectedFileUuid.value))
const clusterAsPending = computed(() => {
console.log('[clusterAsPending] computing, selectedFileUuid:', selectedFileUuid.value, 'clusterResults:', clusterResults.value.length, clusterResults.value)
console.log('[clusterAsPending] clusterResultsCache:', clusterResultsCache.value)
console.log('[clusterAsPending] selectedFileUuid in cache:', selectedFileUuid.value ? (selectedFileUuid.value in clusterResultsCache.value) : 'no file selected')
const faceGroups = computed(() => {
if (!selectedFileUuid.value || !clusterResults.value.length) {
console.log('[clusterAsPending] empty:', { selected: selectedFileUuid.value, clusters: clusterResults.value.length })
return []
}
console.log('[clusterAsPending] mapping', clusterResults.value.length, 'clusters')
return clusterResults.value.map((c: any, idx: number) => {
const traceIds = c.trace_ids || []
const repTraceId = c.representative_trace || traceIds[0]
@@ -656,11 +650,9 @@ const clusterAsPending = computed(() => {
const defaultName = `group ${String(idx + 1).padStart(2, '0')}`
if (!c.name) c.name = defaultName
return {
identity_uuid: `cluster_${c.cluster_id ?? c.group_id}`,
identity_id: `cluster_${String(c.cluster_id ?? c.group_id).padStart(2, '0')}`,
group_uuid: `group_${c.cluster_id ?? c.group_id}`,
group_id: c.cluster_id ?? c.group_id,
name: c.name,
status: 'pending',
starred: false,
trace_count: c.trace_count,
representative_trace: repTraceId,
cluster_id: c.cluster_id ?? c.group_id,
@@ -942,9 +934,9 @@ async function loadClusterResults() {
for (const tid of traceIds) {
const s = suggestions[String(tid)]
if (s) {
const key = s.identity_uuid
const key = s.group_uuid
if (!identityCounts[key]) {
identityCounts[key] = { name: s.name, uuid: s.identity_uuid, count: 0 }
identityCounts[key] = { name: s.name, uuid: s.group_uuid, count: 0 }
}
identityCounts[key].count++
}
@@ -1477,12 +1469,12 @@ async function createNewIdentityFromFace() {
fileUuid: c.file_uuid,
traceIds: c.trace_id ? [c.trace_id] : undefined
})
if (result?.identity_uuid) {
if (result?.group_uuid) {
faceDetailModal.value.show = false
if (c.trace_id) {
unassignedTracesCache.value = unassignedTracesCache.value.filter((t: any) => t.trace_id !== c.trace_id)
}
const cleanUuid = result.identity_uuid.replace(/-/g, '')
const cleanUuid = result.group_uuid.replace(/-/g, '')
router.push(`/people/${cleanUuid}`)
} else {
alert(t('people.detail.create_identity_failed'))
@@ -1518,7 +1510,7 @@ function toggleFaceSelection(t: any) {
}
function toggleGroupSelection(c: any) {
const key = c.identity_uuid
const key = c.group_uuid
const idx = selectedGroups.value.indexOf(key)
if (idx >= 0) {
selectedGroups.value.splice(idx, 1)
@@ -1640,7 +1632,7 @@ async function batchDeleteGroups() {
if (!selectedGroups.value.length) return
const groupIds = Array.from(selectedGroups.value)
const groupsToDelete = clusterAsPending.value.filter((c: any) => groupIds.includes(c.identity_uuid))
const groupsToDelete = faceGroups.value.filter((c: any) => groupIds.includes(c.group_uuid))
let totalTraces = 0
const allTraceIds: number[] = []
@@ -1693,7 +1685,7 @@ async function batchMergeGroups() {
const groupIds = Array.from(selectedGroups.value)
console.log('[batchMergeGroups] groupIds:', groupIds)
mergeCandidateGroups.value = clusterAsPending.value.filter((c: any) => groupIds.includes(c.identity_uuid))
mergeCandidateGroups.value = faceGroups.value.filter((c: any) => groupIds.includes(c.group_uuid))
console.log('[batchMergeGroups] mergeCandidateGroups:', mergeCandidateGroups.value.length)
showMergeTargetModal.value = true
@@ -1703,7 +1695,7 @@ async function executeMerge(targetGroupId: string) {
console.log('[executeMerge] targetGroupId:', targetGroupId)
showMergeTargetModal.value = false
const targetGroup = clusterAsPending.value.find((c: any) => c.identity_uuid === targetGroupId)
const targetGroup = faceGroups.value.find((c: any) => c.group_uuid === targetGroupId)
console.log('[executeMerge] targetGroup:', targetGroup)
if (!targetGroup) return
@@ -1716,7 +1708,7 @@ async function executeMerge(targetGroupId: string) {
const groupIds = Array.from(selectedGroups.value)
const sourceGroupIds = groupIds.filter(id => id !== targetGroupId)
const sourceGroups = clusterAsPending.value.filter((c: any) => sourceGroupIds.includes(c.identity_uuid))
const sourceGroups = faceGroups.value.filter((c: any) => sourceGroupIds.includes(c.group_uuid))
console.log('[executeMerge] sourceGroups:', sourceGroups.length)
const sourceGroupNames = sourceGroups.map(g => g.name).filter(Boolean)
@@ -1837,7 +1829,7 @@ async function createNewGroupAndMove() {
traceIds: traceList.map(t => t.trace_id)
})
if (result?.identity_uuid) {
if (result?.group_uuid) {
clearSelection()
await refresh()
}
@@ -2025,7 +2017,7 @@ async function runDataQC() {
qcProgress.value.status = 'Collecting trace IDs from clusters...'
const allTraceIds = new Set<number>()
for (const c of clusterAsPending.value) {
for (const c of faceGroups.value) {
for (const tid of c.trace_ids || []) {
allTraceIds.add(tid)
}
@@ -2037,7 +2029,7 @@ async function runDataQC() {
qcReport.value.push({
check: 'Traces from Clusters',
status: totalTraces > 0 ? 'PASS' : 'WARN',
details: `${totalTraces} traces in ${clusterAsPending.value.length} clusters`
details: `${totalTraces} FS in ${faceGroups.value.length} groups`
})
// ===== 3. TEST TRACE PROFILES =====
@@ -2054,7 +2046,7 @@ async function runDataQC() {
const trace = clusterTracesMap.value[String(traceId)]
const groupName = traceGroupMap.value[String(traceId)] || 'Unknown'
qcProgress.value.status = `Testing #${traceId} (${groupName})`
qcProgress.value.status = `Testing FS#${traceId} (${groupName})`
const result: { traceId: number; fileUuid: string; groupName: string; canRead: boolean; canWrite: boolean; hasKeyFrame: boolean; hasKeyFace: boolean; error?: string } = {
traceId,
@@ -2112,7 +2104,7 @@ async function runDataQC() {
missingBoth: tested - withKeyFrame - withKeyFace + profileIssues.filter(p => p.hasKeyFrame && p.hasKeyFace).length
}
// Trace profile summary
// FS profile summary
const readFailures = profileIssues.filter(r => !r.canRead).length
const writeFailures = profileIssues.filter(r => !r.canWrite).length
const validTraceCount = tested
@@ -2120,7 +2112,7 @@ async function runDataQC() {
const missingKeyFace = validTraceCount - withKeyFace
qcReport.value.push({
check: 'Trace Profile Read',
check: 'FS Profile Read',
status: readFailures === 0 ? 'PASS' : 'FAIL',
details: readFailures === 0
? `All ${validTraceCount} readable`
@@ -2128,7 +2120,7 @@ async function runDataQC() {
})
qcReport.value.push({
check: 'Trace Profile Write',
check: 'FS Profile Write',
status: writeFailures === 0 ? 'PASS' : 'FAIL',
details: writeFailures === 0
? `All ${validTraceCount} writable`

View File

@@ -263,6 +263,7 @@ const modes = [
{ label: 'Keyword', value: 'keyword', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>' },
{ label: 'Semantic', value: 'semantic', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' },
{ label: 'Agent', value: 'agent', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>' },
{ label: 'People', value: 'people', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>' },
]
const mode = ref(localStorage.getItem('ms_chat_mode_v1') || 'keyword')
@@ -721,15 +722,15 @@ async function sendMessage() {
messages.value[msgIdx].loading = false
if (res?.chips) messages.value[msgIdx].chips = res.chips
} else if (mode.value === 'people') {
const raw: any = await apiCall('search_identities', { query: q, limit: 50 })
const raw: any = await apiCall('search_people', { query: q, limit: 50 })
if (searchSeq !== seq) return
const results = (Array.isArray(raw) ? raw : []).filter((p: any) => p.file_uuid).map((p: any) => ({
file_uuid: p.file_uuid,
file_name: p.name,
summary: `Identity - ${p.source || 'unknown'}`,
const peopleList = raw?.people || []
const results = peopleList.map((p: any) => ({
file_name: p.name || 'Unknown',
summary: `People · ${p.appearance_count || 0} appearances`,
similarity: 1.0,
start_frame: p.startFrame || p.start_frame,
end_frame: p.endFrame || p.end_frame,
start_frame: p.first_appearance_time,
end_frame: p.last_appearance_time,
search_query: q,
}))
messages.value[msgIdx].results = results