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:
2026-10-20 18:06:52 +08:00
parent 55c39e7732
commit c425c29d01

View File

@@ -286,6 +286,7 @@
<script setup lang="ts">
import { ref, nextTick, computed, onMounted, onUnmounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { apiCall } from '@/api'
import { getApiBase } from '@/api/config'
import VideoPlayer from '../components/VideoPlayer.vue'
@@ -325,6 +326,7 @@ const modes = [
const mode = ref(localStorage.getItem('ms_chat_mode_v1') || 'keyword')
const query = ref('')
const messages = ref<ChatMessage[]>([])
const SESSION_STORAGE_KEY = 'ms_search_session'
const thumbs = thumbnailsCache
const sending = ref(false)
const messagesEl = ref<HTMLElement | null>(null)
@@ -598,6 +600,24 @@ onMounted(async () => {
await ensureFiles()
loadHistory()
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
try {
const base = getApiBase()
@@ -616,7 +636,43 @@ onMounted(async () => {
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() {
nextTick(() => {
@@ -633,6 +689,8 @@ function newChat() {
conversationId.value = ''
fileUuidCache.value = []
sending.value = false
// Clear saved session
localStorage.removeItem(SESSION_STORAGE_KEY)
}
function startNewChat() {
@@ -1109,7 +1167,11 @@ async function sendMessage() {
const sceneResults = filteredScenes.map((s: any) => {
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
let vlmDesc = s.vlm_description || ''
@@ -1129,10 +1191,10 @@ async function sendMessage() {
...s,
search_query: q,
source_type: 'scene',
start_frame: frameNumber,
end_frame: frameNumber + 30,
start_time: s.timestamp_sec,
end_time: (s.timestamp_sec || 0) + 1,
start_frame: Math.max(0, frameNumber - Math.round(windowSec * fps)),
end_frame: frameNumber + Math.round(windowSec * fps),
start_time: Math.max(0, ts - windowSec),
end_time: ts + windowSec,
summary: vlmDesc ? `[SCENE] ${vlmDesc}` : `Scene ${sceneIndex}`,
text_content: textContent,
similarity: s.score ?? null,
@@ -1144,7 +1206,11 @@ async function sendMessage() {
})
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
let vlmDesc = t.vlm_description || ''
@@ -1162,8 +1228,10 @@ async function sendMessage() {
...t,
search_query: q,
source_type: 'trace',
start_frame: frame,
end_frame: frame + 30,
start_frame: Math.max(0, frame - Math.round(windowSec * fps)),
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',
text_content: textContent,
similarity: t.score ?? null,