Compare commits
28 Commits
15647a47ac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b944cf9d1 | |||
| 453168f326 | |||
| bcffe62a15 | |||
| ee9a602f7f | |||
| 3aef7d115f | |||
| 19846a8773 | |||
| bb4ff9b007 | |||
| fcb612ff0e | |||
| 1a0b3f2c1f | |||
| 9386a8e8ef | |||
| fb9a55a64c | |||
| 9691e75e36 | |||
| 674126d687 | |||
| d37967d38e | |||
| a289346fdc | |||
| dd843c29ee | |||
| d7592c528c | |||
| f75eb44351 | |||
| 652ba77c77 | |||
| ca94259fba | |||
| 660f01c6c5 | |||
| cb7ebfb633 | |||
| 25f718c0d5 | |||
| 0b19a0af0d | |||
| cf397b268e | |||
| aa25600580 | |||
| 464ddd16a9 | |||
| 4463ff2f96 |
Binary file not shown.
63
src/App.vue
63
src/App.vue
@@ -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>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="ms-ppl-face-grid">
|
||||
<div v-for="c in faceGroups" :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 faceGroups" :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 {
|
||||
@@ -632,15 +632,9 @@ const clusterTracesMap = ref<Record<string, any>>({})
|
||||
const clusterRunning = computed(() => isClusterRunning(selectedFileUuid.value))
|
||||
|
||||
const faceGroups = computed(() => {
|
||||
console.log('[faceGroups] computing, selectedFileUuid:', selectedFileUuid.value, 'clusterResults:', clusterResults.value.length, clusterResults.value)
|
||||
console.log('[faceGroups] clusterResultsCache:', clusterResultsCache.value)
|
||||
console.log('[faceGroups] selectedFileUuid in cache:', selectedFileUuid.value ? (selectedFileUuid.value in clusterResultsCache.value) : 'no file selected')
|
||||
|
||||
if (!selectedFileUuid.value || !clusterResults.value.length) {
|
||||
console.log('[faceGroups] empty:', { selected: selectedFileUuid.value, clusters: clusterResults.value.length })
|
||||
return []
|
||||
}
|
||||
console.log('[faceGroups] mapping', clusterResults.value.length, 'groups')
|
||||
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 faceGroups = 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 = faceGroups.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 = faceGroups.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 = faceGroups.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 = faceGroups.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()
|
||||
}
|
||||
@@ -2037,7 +2029,7 @@ async function runDataQC() {
|
||||
qcReport.value.push({
|
||||
check: 'Traces from Clusters',
|
||||
status: totalTraces > 0 ? 'PASS' : 'WARN',
|
||||
details: `${totalTraces} traces in ${faceGroups.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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user