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">
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user