import { defineStore } from 'pinia' import { ref } from 'vue' import type { SearchResult, SearchRequest } from '@/contracts' import * as searchService from '@/services/searchService' import { ApiErrorClass } from '@/services/apiClient' import { t } from '@/i18n' const VECTOR_ERROR_CODES = new Set([ 'SEMANTIC_INDEX_UNAVAILABLE', 'VECTOR_UNAVAILABLE', 'EMBEDDING_UNAVAILABLE', 'INDEX_UNAVAILABLE', 'MODEL_NOT_FOUND', 'MODEL_CAPABILITY_MISMATCH', 'PROVIDER_UNAVAILABLE', ]) export const useSearchStore = defineStore('search', () => { const query = ref('') const mode = ref<'fts' | 'vector' | 'hybrid'>('hybrid') const results = ref([]) const total = ref(0) const isSearching = ref(false) const selectedIndex = ref(0) const recentQueries = ref([]) const historyError = ref('') let searchVersion = 0 let historyVersion = 0 async function loadHistory() { const version = ++historyVersion try { const response = await searchService.getHistory() if (version !== historyVersion) return recentQueries.value = response.queries historyError.value = '' } catch { if (version === historyVersion) historyError.value = t('无法读取应用搜索记录,请检查后端连接。', 'Could not load search history. Check the backend connection.') } } async function clearHistory() { const version = ++historyVersion try { await searchService.clearHistory() if (version !== historyVersion) return recentQueries.value = []; historyError.value = '' } catch { if (version === historyVersion) historyError.value = t('清空搜索记录失败,请重试。', 'Failed to clear search history. Please retry.') } } const error = ref(null) const vectorUnavailable = ref(false) async function doSearch(request: SearchRequest) { request = { ...request, query: request.query.trim() } if (!request.query) return const version = ++searchVersion query.value = request.query mode.value = request.mode || 'hybrid' isSearching.value = true error.value = null vectorUnavailable.value = false try { const resp = await searchService.search(request) if (version !== searchVersion) return results.value = resp.results total.value = resp.total selectedIndex.value = 0 } catch (reason) { if (version !== searchVersion) return const canFallback = mode.value !== 'fts' && reason instanceof ApiErrorClass && VECTOR_ERROR_CODES.has(reason.code) if (canFallback) { try { const fallback = await searchService.search({ ...request, mode: 'fts' }) if (version !== searchVersion) return results.value = fallback.results total.value = fallback.total mode.value = 'fts' vectorUnavailable.value = true selectedIndex.value = 0 } catch (fallbackError) { if (version !== searchVersion) return error.value = fallbackError instanceof Error ? fallbackError.message : t('全文检索降级失败', 'Full-text search fallback failed') results.value = [] total.value = 0 } } else { error.value = reason instanceof Error ? reason.message : t('搜索失败', 'Search failed') results.value = [] total.value = 0 } } finally { if (version === searchVersion) { isSearching.value = false; await loadHistory() } } } function clearResults() { searchVersion++ isSearching.value = false results.value = [] query.value = '' total.value = 0 selectedIndex.value = 0 error.value = null } function selectNext() { if (selectedIndex.value < results.value.length - 1) selectedIndex.value++ } function selectPrev() { if (selectedIndex.value > 0) selectedIndex.value-- } function setMode(m: 'fts' | 'vector' | 'hybrid') { mode.value = m } return { query, mode, results, total, isSearching, selectedIndex, recentQueries, historyError, clearHistory, loadHistory, error, vectorUnavailable, doSearch, clearResults, selectNext, selectPrev, setMode, } })