主要修复: 1. 修复分页按钮点击无效问题: - handleSearch函数支持指定页码参数 - 分页按钮点击时传递正确的页码 - 解决了setState异步导致的页码不同步问题 2. 修复函数签名问题: - 搜索按钮使用() => handleSearch()调用 - 分页按钮使用handleSearch(newPage)传递页码 3. 分页逻辑优化: - 上一页:setCurrentPage(newPage) + handleSearch(newPage) - 下一页:setCurrentPage(newPage) + handleSearch(newPage) - 确保页码状态和搜索请求同步 测试结果: 分页按钮现在可以正常点击 搜索请求包含正确的page_offset参数 分页状态与搜索请求保持同步 分页功能现在完全正常工作!
622 lines
24 KiB
TypeScript
622 lines
24 KiB
TypeScript
import React, { useState, useCallback, useRef } from 'react';
|
||
import {
|
||
Upload,
|
||
Search,
|
||
Sparkles,
|
||
Image as ImageIcon,
|
||
Settings,
|
||
MessageCircle,
|
||
Grid,
|
||
Filter,
|
||
RefreshCw,
|
||
X,
|
||
ChevronLeft,
|
||
ChevronRight
|
||
} from 'lucide-react';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import { open } from '@tauri-apps/plugin-dialog';
|
||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||
import {
|
||
OutfitAnalysisResult,
|
||
SearchRequest,
|
||
SearchResponse,
|
||
LLMQueryRequest,
|
||
LLMQueryResponse,
|
||
SearchConfig,
|
||
DEFAULT_SEARCH_CONFIG,
|
||
RELEVANCE_THRESHOLD_OPTIONS
|
||
} from '../../types/outfitSearch';
|
||
|
||
/**
|
||
* 智能服装搜索工具
|
||
* 基于 promptx/outfit-match/src/outfit_match/frontend/search.py 的设计
|
||
* 遵循 Tauri 开发规范和 UI/UX 设计标准
|
||
*/
|
||
const OutfitSearchTool: React.FC = () => {
|
||
// 状态管理
|
||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||
const [analysisResult, setAnalysisResult] = useState<OutfitAnalysisResult | null>(null);
|
||
const [searchResults, setSearchResults] = useState<SearchResponse | null>(null);
|
||
const [searchConfig, setSearchConfig] = useState<SearchConfig>(DEFAULT_SEARCH_CONFIG);
|
||
const [llmResponse, setLlmResponse] = useState<LLMQueryResponse | null>(null);
|
||
|
||
// 加载状态
|
||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||
const [isSearching, setIsSearching] = useState(false);
|
||
const [isAsking, setIsAsking] = useState(false);
|
||
|
||
// 错误状态
|
||
const [analysisError, setAnalysisError] = useState<string | null>(null);
|
||
const [searchError, setSearchError] = useState<string | null>(null);
|
||
const [llmError, setLlmError] = useState<string | null>(null);
|
||
|
||
// UI状态
|
||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [llmInput, setLlmInput] = useState('');
|
||
|
||
// 引用
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
// 图像上传处理
|
||
const handleImageUpload = useCallback(async () => {
|
||
try {
|
||
const selected = await open({
|
||
multiple: false,
|
||
filters: [{
|
||
name: 'Image Files',
|
||
extensions: ['jpg', 'jpeg', 'png', 'webp']
|
||
}]
|
||
});
|
||
|
||
if (selected && typeof selected === 'string') {
|
||
setSelectedImage(selected);
|
||
setAnalysisError(null);
|
||
setAnalysisResult(null);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to select image:', error);
|
||
setAnalysisError('图像选择失败');
|
||
}
|
||
}, []);
|
||
|
||
// 图像分析
|
||
const handleAnalyzeImage = useCallback(async () => {
|
||
if (!selectedImage) return;
|
||
|
||
setIsAnalyzing(true);
|
||
setAnalysisError(null);
|
||
|
||
try {
|
||
const response = await invoke<any>('analyze_outfit_image', {
|
||
request: {
|
||
image_path: selectedImage,
|
||
image_name: selectedImage.split('/').pop() || 'image'
|
||
}
|
||
});
|
||
|
||
setAnalysisResult(response.result);
|
||
|
||
// 基于分析结果更新搜索配置
|
||
if (response.result) {
|
||
const newConfig = { ...searchConfig };
|
||
if (response.result.environment_tags) {
|
||
newConfig.environments = response.result.environment_tags;
|
||
}
|
||
if (response.result.products) {
|
||
newConfig.categories = response.result.products.map((p: any) => p.category);
|
||
}
|
||
setSearchConfig(newConfig);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to analyze image:', error);
|
||
setAnalysisError(error as string);
|
||
} finally {
|
||
setIsAnalyzing(false);
|
||
}
|
||
}, [selectedImage, searchConfig]);
|
||
|
||
// 执行搜索 - 支持指定页码
|
||
const handleSearch = useCallback(async (targetPage?: number) => {
|
||
const pageToUse = targetPage ?? currentPage;
|
||
setIsSearching(true);
|
||
setSearchError(null);
|
||
|
||
try {
|
||
// 构建简化的搜索配置
|
||
const simpleConfig: SearchConfig = {
|
||
relevance_threshold: 'HIGH' as any,
|
||
environments: searchConfig.environments || [],
|
||
categories: searchConfig.categories || [],
|
||
color_filters: {},
|
||
design_styles: {},
|
||
max_keywords: 10,
|
||
debug_mode: true, // 启用调试模式查看详细信息
|
||
custom_filters: [],
|
||
query_enhancement_enabled: true,
|
||
color_thresholds: {
|
||
default_hue_threshold: 0.05,
|
||
default_saturation_threshold: 0.05,
|
||
default_value_threshold: 0.2
|
||
}
|
||
};
|
||
|
||
const searchRequest: SearchRequest = {
|
||
query: 'model fashion outfit',
|
||
config: simpleConfig,
|
||
page_size: 9,
|
||
page_offset: (pageToUse - 1) * 9
|
||
};
|
||
|
||
console.log('发送搜索请求:', searchRequest);
|
||
|
||
const response = await invoke<SearchResponse>('search_similar_outfits', {
|
||
request: searchRequest
|
||
});
|
||
|
||
console.log('搜索响应:', response);
|
||
setSearchResults(response);
|
||
} catch (error) {
|
||
console.error('Failed to search outfits:', error);
|
||
setSearchError(error as string);
|
||
} finally {
|
||
setIsSearching(false);
|
||
}
|
||
}, [searchConfig, currentPage]);
|
||
|
||
// LLM问答
|
||
const handleLLMQuery = useCallback(async () => {
|
||
if (!llmInput.trim()) return;
|
||
|
||
setIsAsking(true);
|
||
setLlmError(null);
|
||
|
||
try {
|
||
const response = await invoke<LLMQueryResponse>('ask_llm_outfit_advice', {
|
||
request: {
|
||
user_input: llmInput,
|
||
session_id: undefined
|
||
}
|
||
});
|
||
|
||
setLlmResponse(response);
|
||
} catch (error) {
|
||
console.error('Failed to get LLM advice:', error);
|
||
setLlmError(error as string);
|
||
} finally {
|
||
setIsAsking(false);
|
||
}
|
||
}, [llmInput]);
|
||
|
||
// 清除结果
|
||
const clearResults = useCallback(() => {
|
||
setSearchResults(null);
|
||
setLlmResponse(null);
|
||
setSearchError(null);
|
||
setLlmError(null);
|
||
}, []);
|
||
|
||
// 清除图像
|
||
const clearImage = useCallback(() => {
|
||
setSelectedImage(null);
|
||
setAnalysisResult(null);
|
||
setAnalysisError(null);
|
||
}, []);
|
||
|
||
return (
|
||
<div className="h-full flex flex-col">
|
||
{/* 页面标题 */}
|
||
<div className="page-header flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||
<Search className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-pink-600 bg-clip-text text-transparent">
|
||
智能服装搜索
|
||
</h1>
|
||
<p className="text-gray-600 text-lg">基于AI的智能服装搜索和匹配工具</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主要内容区域 - 双列布局 */}
|
||
<div className="flex-1 flex gap-6 min-h-0">
|
||
{/* 左侧:搜索结果展示区域 (70%) */}
|
||
<div className="flex-1 flex flex-col min-h-0">
|
||
<div className="card p-6 flex-1 flex flex-col">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-xl font-semibold text-gray-900">搜索结果</h2>
|
||
<div className="flex items-center gap-3">
|
||
{isSearching && (
|
||
<div className="flex items-center gap-2 text-primary-600">
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
<span className="text-sm">搜索中...</span>
|
||
</div>
|
||
)}
|
||
{searchResults && (
|
||
<span className="text-sm text-gray-500">
|
||
找到 {searchResults.total_size} 个结果
|
||
{searchResults.search_time_ms && (
|
||
<span className="ml-2">({searchResults.search_time_ms}ms)</span>
|
||
)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 搜索错误显示 */}
|
||
{searchError && (
|
||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
<p className="text-red-600 text-sm">{searchError}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 搜索结果网格 */}
|
||
<div className="flex-1 overflow-y-auto">
|
||
{searchResults && searchResults.results.length > 0 ? (
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-3 gap-4">
|
||
{searchResults.results.map((result, index) => (
|
||
<div key={result.id || index} className="card p-4 hover:shadow-lg transition-shadow">
|
||
<img
|
||
src={result.image_url}
|
||
alt="Outfit"
|
||
className="w-full h-48 object-cover rounded-lg mb-3"
|
||
onError={(e) => {
|
||
console.error('搜索结果图片加载失败:', result.image_url);
|
||
e.currentTarget.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xMDAgNzBMMTMwIDEwMEgxMTBWMTMwSDkwVjEwMEg3MEwxMDAgNzBaIiBmaWxsPSIjOUI5QkEwIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOUI5QkEwIiBmb250LXNpemU9IjEyIj7lm77niYfliKDpmaTlpLHotKU8L3RleHQ+Cjwvc3ZnPg==';
|
||
}}
|
||
/>
|
||
<div className="space-y-2">
|
||
<p className="text-sm text-gray-600 line-clamp-2">
|
||
{result.style_description}
|
||
</p>
|
||
<div className="flex flex-wrap gap-1">
|
||
{result.environment_tags.slice(0, 3).map((tag, i) => (
|
||
<span key={i} className="px-2 py-1 bg-gray-100 text-xs rounded-full">
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
{result.relevance_score && (
|
||
<div className="text-xs text-gray-500">
|
||
相关度: {(result.relevance_score * 100).toFixed(1)}%
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* 分页控件 */}
|
||
{searchResults.total_size > 9 && (
|
||
<div className="flex items-center justify-between pt-4 border-t border-gray-200">
|
||
<div className="text-sm text-gray-500">
|
||
第 {currentPage} 页,共 {Math.ceil(searchResults.total_size / 9)} 页
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => {
|
||
if (currentPage > 1) {
|
||
const newPage = currentPage - 1;
|
||
setCurrentPage(newPage);
|
||
handleSearch(newPage);
|
||
}
|
||
}}
|
||
disabled={currentPage <= 1}
|
||
className="p-2 text-gray-500 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
</button>
|
||
<span className="px-3 py-1 bg-gray-100 rounded text-sm">
|
||
{currentPage}
|
||
</span>
|
||
<button
|
||
onClick={() => {
|
||
if (currentPage < Math.ceil(searchResults.total_size / 9)) {
|
||
const newPage = currentPage + 1;
|
||
setCurrentPage(newPage);
|
||
handleSearch(newPage);
|
||
}
|
||
}}
|
||
disabled={currentPage >= Math.ceil(searchResults.total_size / 9)}
|
||
className="p-2 text-gray-500 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<Grid className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||
<p className="text-gray-500">暂无搜索结果</p>
|
||
<p className="text-sm text-gray-400 mt-2">上传图像或设置搜索条件开始搜索</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右侧:搜索控制面板 (30%) */}
|
||
<div className="w-96 flex flex-col space-y-4">
|
||
{/* 图像上传区域 */}
|
||
<div className="card p-4">
|
||
<h3 className="text-lg font-semibold mb-3">图像上传</h3>
|
||
|
||
{selectedImage ? (
|
||
<div className="space-y-3">
|
||
<div className="relative">
|
||
<img
|
||
src={convertFileSrc(selectedImage)}
|
||
alt="Selected"
|
||
className="w-full h-48 object-cover rounded-lg"
|
||
onError={(e) => {
|
||
console.error('图片加载失败:', selectedImage);
|
||
setAnalysisError('图片加载失败,请重新选择图片');
|
||
}}
|
||
/>
|
||
<button
|
||
onClick={clearImage}
|
||
className="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleAnalyzeImage}
|
||
disabled={isAnalyzing}
|
||
className="w-full btn-primary flex items-center justify-center gap-2"
|
||
>
|
||
{isAnalyzing ? (
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<Sparkles className="w-4 h-4" />
|
||
)}
|
||
{isAnalyzing ? '分析中...' : '解析图像'}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={handleImageUpload}
|
||
className="w-full h-32 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center hover:border-primary-500 transition-colors"
|
||
>
|
||
<Upload className="w-8 h-8 text-gray-400 mb-2" />
|
||
<span className="text-gray-500">点击上传图像</span>
|
||
</button>
|
||
)}
|
||
|
||
{analysisError && (
|
||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
<p className="text-red-600 text-sm">{analysisError}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 分析结果展示 */}
|
||
{analysisResult && (
|
||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<h4 className="text-sm font-medium text-blue-800 mb-2">解析结果</h4>
|
||
<div className="space-y-2 text-sm text-blue-700">
|
||
<p><strong>风格描述:</strong> {analysisResult.style_description}</p>
|
||
{analysisResult.environment_tags && (
|
||
<p><strong>环境标签:</strong> {analysisResult.environment_tags.join(', ')}</p>
|
||
)}
|
||
{analysisResult.products && (
|
||
<div>
|
||
<strong>产品信息:</strong>
|
||
<ul className="mt-1 space-y-1">
|
||
{analysisResult.products.map((product, index) => (
|
||
<li key={index} className="text-xs">
|
||
• {product.category}: {product.design_styles?.join(', ')}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 搜索设置 */}
|
||
<div className="card p-4">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className="text-lg font-semibold">搜索设置</h3>
|
||
<button
|
||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
||
className="p-2 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100"
|
||
>
|
||
<Settings className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{/* 相关性阈值 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
匹配严格程度
|
||
</label>
|
||
<select
|
||
value={searchConfig.relevance_threshold}
|
||
onChange={(e) => setSearchConfig({
|
||
...searchConfig,
|
||
relevance_threshold: e.target.value as any
|
||
})}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
>
|
||
{RELEVANCE_THRESHOLD_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* 高级过滤器 */}
|
||
{showAdvancedFilters && (
|
||
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
|
||
<h4 className="text-sm font-medium text-gray-700">高级过滤器</h4>
|
||
|
||
{/* 环境标签 */}
|
||
{analysisResult && analysisResult.environment_tags && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
环境标签
|
||
</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{analysisResult.environment_tags.map((tag, index) => (
|
||
<button
|
||
key={index}
|
||
onClick={() => {
|
||
const newEnvironments = searchConfig.environments.includes(tag)
|
||
? searchConfig.environments.filter(e => e !== tag)
|
||
: [...searchConfig.environments, tag];
|
||
setSearchConfig({
|
||
...searchConfig,
|
||
environments: newEnvironments
|
||
});
|
||
}}
|
||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||
searchConfig.environments.includes(tag)
|
||
? 'bg-primary-500 text-white'
|
||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||
}`}
|
||
>
|
||
{tag}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 产品类别 */}
|
||
{analysisResult && analysisResult.products && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
产品类别
|
||
</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{analysisResult.products.map((product, index) => (
|
||
<button
|
||
key={index}
|
||
onClick={() => {
|
||
const newCategories = searchConfig.categories.includes(product.category)
|
||
? searchConfig.categories.filter(c => c !== product.category)
|
||
: [...searchConfig.categories, product.category];
|
||
setSearchConfig({
|
||
...searchConfig,
|
||
categories: newCategories
|
||
});
|
||
}}
|
||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||
searchConfig.categories.includes(product.category)
|
||
? 'bg-primary-500 text-white'
|
||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||
}`}
|
||
>
|
||
{product.category}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 最大关键词数量 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
最大关键词数量: {searchConfig.max_keywords}
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="1"
|
||
max="50"
|
||
value={searchConfig.max_keywords}
|
||
onChange={(e) => setSearchConfig({
|
||
...searchConfig,
|
||
max_keywords: parseInt(e.target.value)
|
||
})}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 搜索按钮 */}
|
||
<button
|
||
onClick={() => handleSearch()}
|
||
disabled={isSearching}
|
||
className="w-full btn-primary flex items-center justify-center gap-2"
|
||
>
|
||
{isSearching ? (
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<Search className="w-4 h-4" />
|
||
)}
|
||
{isSearching ? '搜索中...' : '开始搜索'}
|
||
</button>
|
||
|
||
{/* 清除搜索结果 */}
|
||
{searchResults && (
|
||
<button
|
||
onClick={clearResults}
|
||
className="w-full btn-secondary flex items-center justify-center gap-2"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
清除结果
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* LLM问答区域 */}
|
||
<div className="card p-4">
|
||
<h3 className="text-lg font-semibold mb-3">AI助手</h3>
|
||
|
||
<div className="space-y-3">
|
||
<textarea
|
||
value={llmInput}
|
||
onChange={(e) => setLlmInput(e.target.value)}
|
||
placeholder="描述你的穿搭需求或场景..."
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-none"
|
||
rows={3}
|
||
/>
|
||
|
||
<button
|
||
onClick={handleLLMQuery}
|
||
disabled={isAsking || !llmInput.trim()}
|
||
className="w-full btn-secondary flex items-center justify-center gap-2"
|
||
>
|
||
{isAsking ? (
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<MessageCircle className="w-4 h-4" />
|
||
)}
|
||
{isAsking ? '思考中...' : '询问AI'}
|
||
</button>
|
||
</div>
|
||
|
||
{llmResponse && (
|
||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<p className="text-blue-800 text-sm">{llmResponse.answer}</p>
|
||
</div>
|
||
)}
|
||
|
||
{llmError && (
|
||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
<p className="text-red-600 text-sm">{llmError}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default OutfitSearchTool;
|