Files
mixvideo-v2/apps/desktop/src/pages/tools/SimilaritySearchTool.tsx
imeepos 90aa401059 fix: 修复相似度检索工具事件冒泡问题和Rust编译错误
- 修复SimilaritySearchCard中外部链接按钮点击时触发卡片选择的事件冒泡问题
- 增强外部链接按钮事件处理,添加preventDefault和stopPropagation
- 改进卡片点击事件处理,检查点击目标避免按钮触发卡片选择
- 修复outfit_search_commands.rs中的lifetime错误,重构描述字段解析逻辑
- 移除相关性阈值过滤逻辑,返回所有搜索结果
- 优化相似度检索工具UI布局和详情模态框功能
- 添加详细的搜索结果展示和外部链接处理功能
2025-07-24 16:45:57 +08:00

415 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useCallback, useState } from 'react';
import {
Search,
Sparkles,
RotateCcw,
TrendingUp,
Zap,
ArrowLeft,
Loader2,
X,
ExternalLink,
Copy,
Download
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
useSimilaritySearchStore,
useSimilaritySearchSelectors,
useSimilaritySearchActions
} from '../../store/similaritySearchStore';
import { SimilaritySearchPanel } from '../../components/similarity/SimilaritySearchPanel';
import SimilaritySearchResults from '../../components/similarity/SimilaritySearchResults';
import { SimilaritySearchRequest } from '../../types/similaritySearch';
import { CustomSelect } from '../../components/CustomSelect';
/**
* 相似度检索工具页面
* 遵循 Tauri 开发规范和 UI/UX 设计标准
*/
const SimilaritySearchTool: React.FC = () => {
const navigate = useNavigate();
// 本地状态
const [selectedResult, setSelectedResult] = useState<any>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
// 状态管理
const {
query,
selectedThreshold,
setQuery,
setThreshold,
executeSearch,
searchWithPagination,
changePageSize,
loadConfig,
clearError,
setShowSuggestions,
} = useSimilaritySearchStore();
// 选择器
const { useSearchState, useConfigState, useSuggestionsState } = useSimilaritySearchSelectors();
const { quickSearch, resetAll, selectSuggestionAndSearch } = useSimilaritySearchActions();
// 获取状态
const searchState = useSearchState();
const configState = useConfigState();
const suggestionsState = useSuggestionsState();
// 页面初始化
useEffect(() => {
loadConfig();
clearError();
}, [loadConfig, clearError]);
// 恢复持久化的查询内容
useEffect(() => {
const persistedData = localStorage.getItem('similarity-search-storage');
if (persistedData) {
try {
const parsed = JSON.parse(persistedData);
const persistedQuery = parsed.state?.query;
if (persistedQuery && persistedQuery.trim() && persistedQuery !== query) {
console.log('恢复持久化的查询内容:', persistedQuery);
setQuery(persistedQuery);
}
} catch (e) {
console.warn('无法解析持久化的查询内容:', e);
}
}
}, []); // 只在组件挂载时执行一次
// 处理搜索
const handleSearch = useCallback((request: SimilaritySearchRequest) => {
executeSearch(request);
}, [executeSearch]);
// 处理建议选择
const handleSuggestionSelect = useCallback((suggestion: string) => {
selectSuggestionAndSearch(suggestion);
}, [selectSuggestionAndSearch]);
// 处理快速搜索标签点击
const handleQuickTagClick = useCallback((tag: string) => {
quickSearch(tag);
}, [quickSearch]);
// 处理重置
const handleReset = useCallback(() => {
// 清除持久化数据
localStorage.removeItem('similarity-search-storage');
// 重置所有状态
resetAll();
}, [resetAll]);
// 处理分页
const handlePageChange = useCallback((page: number) => {
searchWithPagination(page);
}, [searchWithPagination]);
// 处理每页显示数量变化
const handlePageSizeChange = useCallback((pageSize: number) => {
changePageSize(pageSize);
}, [changePageSize]);
// 处理结果选择
const handleResultSelect = useCallback((result: any) => {
console.log('Selected result:', result);
setSelectedResult(result);
setShowDetailModal(true);
}, []);
// 处理外部链接打开
const handleOpenExternal = useCallback((url: string) => {
// 直接使用 window.open 打开外部链接
window.open(url, '_blank', 'noopener,noreferrer');
}, []);
// 关闭详细信息模态框
const handleCloseDetailModal = useCallback(() => {
setShowDetailModal(false);
setSelectedResult(null);
}, []);
// 监听相关性阈值变化,立即重新搜索
useEffect(() => {
const request: SimilaritySearchRequest = {
query: query.trim(),
relevance_threshold: selectedThreshold,
page_size: configState.config?.max_results_per_page || 12,
page_offset: (searchState.currentPage - 1) * (configState.config?.max_results_per_page || 12),
};
executeSearch(request);
}, [selectedThreshold]); // 只监听阈值变化
// 返回工具列表
const handleBackToTools = useCallback(() => {
navigate('/tools');
}, [navigate]);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-50">
{/* 页面头部 */}
<div className="bg-white border-b border-gray-100 shadow-sm">
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleBackToTools}
className="flex items-center gap-2 py-2 text-gray-600 hover:text-primary-600 hover:bg-gray-50 rounded-lg transition-all duration-200"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
<div className="h-6 w-px bg-gray-200"></div>
<div className="flex items-center gap-3">
<div className="icon-container primary w-10 h-10">
<Search className="w-5 h-5" />
</div>
<div>
<h1 className="text-2xl font-bold text-high-emphasis"></h1>
<p className="text-sm text-medium-emphasis">AI的智能相似度搜索</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleReset}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-200"
>
<RotateCcw className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
</div>
</div>
</div>
</div>
{/* 全局搜索状态指示器 */}
{searchState.isSearching && (
<div className="bg-primary-50 border-b border-primary-200">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div className="flex items-center justify-center gap-3">
<Loader2 className="w-5 h-5 animate-spin text-primary-600" />
<span className="text-sm font-medium text-primary-900">...</span>
<span className="text-xs text-primary-700">AI正在为您分析匹配结果</span>
</div>
</div>
</div>
)}
{/* 主要内容 */}
<div className="mx-auto py-6 lg:py-8">
{/* 搜索面板和结果 */}
<div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-6 lg:gap-8">
{/* 搜索面板 */}
<div className="space-y-6 order-2 lg:order-1">
<SimilaritySearchPanel
query={query}
selectedThreshold={selectedThreshold}
suggestions={suggestionsState.suggestions}
showSuggestions={suggestionsState.showSuggestions}
isSearching={searchState.isSearching}
onQueryChange={setQuery}
onSearch={handleSearch}
onSuggestionSelect={handleSuggestionSelect}
onSuggestionsToggle={setShowSuggestions}
/>
</div>
{/* 搜索结果 */}
<div className="min-h-[400px] sm:min-h-[600px] order-1 lg:order-2">
{searchState.results.length > 0 ? (
<SimilaritySearchResults
results={searchState.results}
totalResults={searchState.totalResults}
currentPage={searchState.currentPage}
maxResultsPerPage={configState.config?.max_results_per_page || 12}
isLoading={searchState.isSearching}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onResultSelect={handleResultSelect}
onExternalLinkClick={handleOpenExternal}
/>
) : searchState.error ? (
<div className="card h-full flex flex-col items-center justify-center text-center p-6 sm:p-12 animate-fade-in bg-gradient-to-br from-red-50 to-pink-50 border border-red-200">
<div className="icon-container red w-16 h-16 mb-6 shadow-md">
<Search className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-red-900 mb-3"> </h3>
<p className="text-red-700 mb-6 max-w-md leading-relaxed">{searchState.error}</p>
<button
onClick={handleReset}
className="btn btn-primary hover:shadow-md transition-all duration-200"
>
🔄
</button>
</div>
) : (
<div className="card h-full flex flex-col items-center justify-center text-center p-6 sm:p-12 animate-fade-in bg-gradient-to-br from-purple-50 to-indigo-50 border border-purple-200">
<div className="icon-container purple w-16 h-16 mb-6 shadow-md">
<TrendingUp className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-purple-900 mb-3"> </h3>
<div className="space-y-2 text-purple-700 max-w-md">
<p className="flex items-center justify-center gap-2">
<Search className="w-4 h-4" />
</p>
<p className="flex items-center justify-center gap-2">
<Zap className="w-4 h-4" />
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* 详细信息模态框 */}
{showDetailModal && selectedResult && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
{/* 模态框头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h3 className="text-xl font-semibold text-gray-900"></h3>
<button
onClick={handleCloseDetailModal}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
{/* 模态框内容 */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
<div className="space-y-6">
{/* 图片展示 */}
<div className="relative">
<img
src={selectedResult.image_url}
alt={selectedResult.style_description}
className="w-full h-64 object-cover rounded-lg"
/>
<div className="absolute top-3 right-3">
<div className="px-3 py-1 bg-white/90 backdrop-blur-sm rounded-full text-sm font-medium">
: {(selectedResult.relevance_score * 100).toFixed(1)}%
</div>
</div>
</div>
{/* 基本信息 */}
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<p className="text-gray-900 bg-gray-50 p-3 rounded-lg">
{selectedResult.style_description || '暂无描述'}
</p>
</div>
{/* 环境标签 */}
{selectedResult.environment_tags && selectedResult.environment_tags.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<div className="flex flex-wrap gap-2">
{selectedResult.environment_tags.map((tag: string, index: number) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
</div>
)}
{/* 产品信息 */}
{selectedResult.products && selectedResult.products.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<div className="space-y-3">
{selectedResult.products.map((product: any, index: number) => (
<div key={index} className="bg-gray-50 p-4 rounded-lg border border-gray-200">
{/* 产品类别 */}
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-semibold text-gray-900">{product.category}</span>
{product.color_pattern?.rgb_hex && (
<div
className="w-4 h-4 rounded-full border border-gray-300"
style={{ backgroundColor: product.color_pattern.rgb_hex }}
title={`颜色: ${product.color_pattern.rgb_hex}`}
/>
)}
</div>
{/* 产品描述 */}
{product.description && product.description !== "时尚单品" && (
<div className="text-sm text-gray-700 mb-2">{product.description}</div>
)}
{/* 设计风格 */}
{product.design_styles && product.design_styles.length > 0 && (
<div className="flex flex-wrap gap-1">
{product.design_styles.map((style: string, styleIndex: number) => (
<span
key={styleIndex}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{style}
</span>
))}
</div>
)}
{/* 颜色信息详细 */}
{product.color_pattern && (
<div className="mt-2 text-xs text-gray-500">
<div className="grid grid-cols-3 gap-2">
<span>: {(product.color_pattern.hue * 360).toFixed(0)}°</span>
<span>: {(product.color_pattern.saturation * 100).toFixed(0)}%</span>
<span>: {(product.color_pattern.value * 100).toFixed(0)}%</span>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex gap-3 pt-4 border-t border-gray-200">
<button
onClick={() => handleOpenExternal(selectedResult.image_url)}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
<ExternalLink className="w-4 h-4" />
</button>
<button
onClick={() => navigator.clipboard.writeText(selectedResult.image_url)}
className="flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<Copy className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default SimilaritySearchTool;