feat: persist search session across page navigation
- Save messages to localStorage when leaving search page - Restore messages when returning to search page - Clear session on newChat() - Session includes: messages, conversationId, fileUuidCache, mode
This commit is contained in:
@@ -286,6 +286,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, nextTick, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { onBeforeRouteLeave } from 'vue-router'
|
||||||
import { apiCall } from '@/api'
|
import { apiCall } from '@/api'
|
||||||
import { getApiBase } from '@/api/config'
|
import { getApiBase } from '@/api/config'
|
||||||
import VideoPlayer from '../components/VideoPlayer.vue'
|
import VideoPlayer from '../components/VideoPlayer.vue'
|
||||||
@@ -325,6 +326,7 @@ const modes = [
|
|||||||
const mode = ref(localStorage.getItem('ms_chat_mode_v1') || 'keyword')
|
const mode = ref(localStorage.getItem('ms_chat_mode_v1') || 'keyword')
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const messages = ref<ChatMessage[]>([])
|
const messages = ref<ChatMessage[]>([])
|
||||||
|
const SESSION_STORAGE_KEY = 'ms_search_session'
|
||||||
const thumbs = thumbnailsCache
|
const thumbs = thumbnailsCache
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
const messagesEl = ref<HTMLElement | null>(null)
|
const messagesEl = ref<HTMLElement | null>(null)
|
||||||
@@ -598,6 +600,24 @@ onMounted(async () => {
|
|||||||
await ensureFiles()
|
await ensureFiles()
|
||||||
loadHistory()
|
loadHistory()
|
||||||
loadBookmarks()
|
loadBookmarks()
|
||||||
|
|
||||||
|
// Restore session from localStorage
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(SESSION_STORAGE_KEY)
|
||||||
|
if (saved) {
|
||||||
|
const session = JSON.parse(saved)
|
||||||
|
if (session.messages && Array.isArray(session.messages)) {
|
||||||
|
messages.value = session.messages
|
||||||
|
conversationId.value = session.conversationId || ''
|
||||||
|
fileUuidCache.value = session.fileUuidCache || []
|
||||||
|
mode.value = session.mode || mode.value
|
||||||
|
localStorage.setItem('ms_chat_mode_v1', mode.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SearchView] Failed to restore session:', e)
|
||||||
|
}
|
||||||
|
|
||||||
// Load search config for visual search parameters
|
// Load search config for visual search parameters
|
||||||
try {
|
try {
|
||||||
const base = getApiBase()
|
const base = getApiBase()
|
||||||
@@ -616,7 +636,43 @@ onMounted(async () => {
|
|||||||
console.error('[SearchView] Failed to load search config:', e)
|
console.error('[SearchView] Failed to load search config:', e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onUnmounted(() => document.removeEventListener('click', closeDropdowns))
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
// Save current session to localStorage
|
||||||
|
try {
|
||||||
|
if (messages.value.length > 0) {
|
||||||
|
const session = {
|
||||||
|
messages: messages.value,
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
fileUuidCache: fileUuidCache.value,
|
||||||
|
mode: mode.value,
|
||||||
|
savedAt: Date.now()
|
||||||
|
}
|
||||||
|
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SearchView] Failed to save session:', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', closeDropdowns)
|
||||||
|
// Save session on unmount as well
|
||||||
|
try {
|
||||||
|
if (messages.value.length > 0) {
|
||||||
|
const session = {
|
||||||
|
messages: messages.value,
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
fileUuidCache: fileUuidCache.value,
|
||||||
|
mode: mode.value,
|
||||||
|
savedAt: Date.now()
|
||||||
|
}
|
||||||
|
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SearchView] Failed to save session:', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -633,6 +689,8 @@ function newChat() {
|
|||||||
conversationId.value = ''
|
conversationId.value = ''
|
||||||
fileUuidCache.value = []
|
fileUuidCache.value = []
|
||||||
sending.value = false
|
sending.value = false
|
||||||
|
// Clear saved session
|
||||||
|
localStorage.removeItem(SESSION_STORAGE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNewChat() {
|
function startNewChat() {
|
||||||
@@ -1109,7 +1167,11 @@ async function sendMessage() {
|
|||||||
|
|
||||||
const sceneResults = filteredScenes.map((s: any) => {
|
const sceneResults = filteredScenes.map((s: any) => {
|
||||||
const sceneIndex = s.index ?? s.scene_index ?? 0
|
const sceneIndex = s.index ?? s.scene_index ?? 0
|
||||||
const frameNumber = s.frame_number ?? Math.round((s.timestamp_sec || 0) * 30)
|
const fileInfo = filesCache.value.find((f: any) => (f.file_uuid || f.uuid) === s.file_uuid)
|
||||||
|
const fps = fileInfo?.fps || 30
|
||||||
|
const ts = s.timestamp_sec ?? (s.frame_number != null ? s.frame_number / fps : 0)
|
||||||
|
const frameNumber = s.frame_number ?? Math.round(ts * fps)
|
||||||
|
const windowSec = 5
|
||||||
|
|
||||||
// Build bilingual description
|
// Build bilingual description
|
||||||
let vlmDesc = s.vlm_description || ''
|
let vlmDesc = s.vlm_description || ''
|
||||||
@@ -1129,10 +1191,10 @@ async function sendMessage() {
|
|||||||
...s,
|
...s,
|
||||||
search_query: q,
|
search_query: q,
|
||||||
source_type: 'scene',
|
source_type: 'scene',
|
||||||
start_frame: frameNumber,
|
start_frame: Math.max(0, frameNumber - Math.round(windowSec * fps)),
|
||||||
end_frame: frameNumber + 30,
|
end_frame: frameNumber + Math.round(windowSec * fps),
|
||||||
start_time: s.timestamp_sec,
|
start_time: Math.max(0, ts - windowSec),
|
||||||
end_time: (s.timestamp_sec || 0) + 1,
|
end_time: ts + windowSec,
|
||||||
summary: vlmDesc ? `[SCENE] ${vlmDesc}` : `Scene ${sceneIndex}`,
|
summary: vlmDesc ? `[SCENE] ${vlmDesc}` : `Scene ${sceneIndex}`,
|
||||||
text_content: textContent,
|
text_content: textContent,
|
||||||
similarity: s.score ?? null,
|
similarity: s.score ?? null,
|
||||||
@@ -1144,7 +1206,11 @@ async function sendMessage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const traceResults = filteredTraces.map((t: any) => {
|
const traceResults = filteredTraces.map((t: any) => {
|
||||||
const frame = t.start_frame ?? t.best_face_frame ?? 0
|
const fileInfo = filesCache.value.find((f: any) => (f.file_uuid || f.uuid) === t.file_uuid)
|
||||||
|
const fps = fileInfo?.fps || 30
|
||||||
|
const frame = t.start_frame ?? t.best_face_frame ?? t.frame_number ?? Math.round((t.timestamp_sec || 0) * fps)
|
||||||
|
const ts = t.start_time ?? t.timestamp_sec ?? (frame / fps)
|
||||||
|
const windowSec = 5
|
||||||
|
|
||||||
// Build bilingual description
|
// Build bilingual description
|
||||||
let vlmDesc = t.vlm_description || ''
|
let vlmDesc = t.vlm_description || ''
|
||||||
@@ -1162,8 +1228,10 @@ async function sendMessage() {
|
|||||||
...t,
|
...t,
|
||||||
search_query: q,
|
search_query: q,
|
||||||
source_type: 'trace',
|
source_type: 'trace',
|
||||||
start_frame: frame,
|
start_frame: Math.max(0, frame - Math.round(windowSec * fps)),
|
||||||
end_frame: frame + 30,
|
end_frame: frame + Math.round(windowSec * fps),
|
||||||
|
start_time: Math.max(0, ts - windowSec),
|
||||||
|
end_time: ts + windowSec,
|
||||||
summary: vlmDesc || t.text_content || 'Person appearance',
|
summary: vlmDesc || t.text_content || 'Person appearance',
|
||||||
text_content: textContent,
|
text_content: textContent,
|
||||||
similarity: t.score ?? null,
|
similarity: t.score ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user