feat: 完善聊天界面功能和用户体验

界面优化:
- 修复页面头部信息重复问题,统一为时尚穿搭顾问
- 优化欢迎界面布局,移除重复标题
- 调整标签激活状态样式,使其更加subtle和优雅

 标签系统增强:
- 实现卡片标签与底部工具栏标签完美同步
- 修复标签选中状态的实时更新问题
- 添加气泡卡片展示所有标签详情
- 优化标签折叠功能,默认隐藏,一行布局

 功能完善:
- 修复输入框和发送按钮对齐问题
- 优化聊天输入框固定到底部
- 将弹框改为更优雅的气泡卡片消息
- 生成搜索和清空按钮移到外层,始终可见

 导航优化:
- 服装搭配导航直接链接到AI智能聊天
- 配置独立路由避免与便捷工具冲突
- 移除原有的服装搭配页面和路由

 用户体验:
- 标签选中状态双向同步更新
- 气泡卡片支持标签选择和实时反馈
- 优化加载状态显示和错误处理
- 完善的视觉反馈和交互动画
This commit is contained in:
imeepos
2025-07-21 23:35:04 +08:00
parent 07ecd9cee7
commit 94e9f74b15
4 changed files with 346 additions and 214 deletions

View File

@@ -15,7 +15,7 @@ import JsonParserTool from './pages/tools/JsonParserTool';
import DebugPanelTool from './pages/tools/DebugPanelTool';
import ChatTool from './pages/tools/ChatTool';
import ChatTestPage from './pages/tools/ChatTestPage';
import OutfitMatch from './pages/OutfitMatch';
import Navigation from './components/Navigation';
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
import { useProjectStore } from './store/projectStore';
@@ -91,7 +91,8 @@ function App() {
<Route path="/ai-classification-settings" element={<AiClassificationSettings />} />
<Route path="/templates" element={<TemplateManagement />} />
<Route path="/material-model-binding" element={<MaterialModelBinding />} />
<Route path="/outfit-match" element={<OutfitMatch />} />
<Route path="/fashion-chat" element={<ChatTool />} />
<Route path="/tools" element={<Tools />} />
<Route path="/tools/data-cleaning" element={<DataCleaningTool />} />
<Route path="/tools/json-parser" element={<JsonParserTool />} />

View File

@@ -10,7 +10,6 @@ import {
Clock,
CheckCircle,
XCircle,
Terminal,
Copy,
ExternalLink,
Tag,
@@ -19,7 +18,7 @@ import {
MapPin
} from 'lucide-react';
import { queryRagGrounding } from '../services/ragGroundingService';
import { RagGroundingQueryOptions, RagGroundingResponse } from '../types/ragGrounding';
import { RagGroundingQueryOptions } from '../types/ragGrounding';
/**
* 聊天消息接口
@@ -77,6 +76,10 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
const [showAnswers, setShowAnswers] = useState<Record<string, boolean>>({});
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [allTags, setAllTags] = useState<string[]>([]);
const [isTagsExpanded, setIsTagsExpanded] = useState(false);
const [showTagBubble, setShowTagBubble] = useState(false);
const [bubbleTags, setBubbleTags] = useState<{categories: string[], environment: string[]}>({categories: [], environment: []});
const [bubblePosition, setBubblePosition] = useState<{x: number, y: number}>({x: 0, y: 0});
const inputRef = useRef<HTMLTextAreaElement>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);
@@ -192,17 +195,37 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
{/* 分类标签 */}
{imageData.categories.length > 0 && (
<div className="flex flex-wrap gap-1">
{imageData.categories.slice(0, 4).map((category: string, catIndex: number) => (
<span
key={catIndex}
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full"
>
<Tag className="w-2 h-2" />
{category}
</span>
))}
{imageData.categories.slice(0, 4).map((category: string, catIndex: number) => {
const isSelected = selectedTags.includes(category);
return (
<button
key={catIndex}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleTag(category);
}}
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full transition-all duration-200 border ${
isSelected
? 'bg-pink-100 text-pink-700 border-pink-300 shadow-sm'
: 'bg-blue-50 text-blue-700 hover:bg-pink-100 hover:text-pink-700 border-blue-200 hover:border-pink-300'
}`}
>
<Tag className="w-2 h-2" />
{category}
</button>
);
})}
{imageData.categories.length > 4 && (
<span className="text-xs text-gray-500">+{imageData.categories.length - 4}</span>
<button
onClick={(e) => {
e.stopPropagation();
showTagDetails(imageData, e);
}}
className="text-xs text-gray-500 hover:text-pink-600 hover:bg-pink-50 px-2 py-1 rounded-full transition-all duration-200 border border-gray-300 hover:border-pink-300"
>
+{imageData.categories.length - 4}
</button>
)}
</div>
)}
@@ -210,15 +233,27 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
{/* 环境标签 */}
{imageData.environment_tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{imageData.environment_tags.slice(0, 3).map((tag: string, tagIndex: number) => (
<span
key={tagIndex}
className="inline-flex items-center gap-1 px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full"
>
<MapPin className="w-2 h-2" />
{tag}
</span>
))}
{imageData.environment_tags.slice(0, 3).map((tag: string, tagIndex: number) => {
const isSelected = selectedTags.includes(tag);
return (
<button
key={tagIndex}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleTag(tag);
}}
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full transition-all duration-200 border ${
isSelected
? 'bg-pink-100 text-pink-700 border-pink-300 shadow-sm'
: 'bg-green-50 text-green-700 hover:bg-pink-100 hover:text-pink-700 border-green-200 hover:border-pink-300'
}`}
>
<MapPin className="w-2 h-2" />
{tag}
</button>
);
})}
</div>
)}
@@ -253,7 +288,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
</div>
</div>
);
}, [parseImageContent]);
}, [parseImageContent, selectedTags]);
// 生成消息ID
const generateMessageId = useCallback(() => {
@@ -448,11 +483,11 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
const imageData = parseImageContent(source.content);
if (imageData) {
// 添加分类标签
imageData.categories.forEach(tag => tagSet.add(tag));
imageData.categories.forEach((tag: string) => tagSet.add(tag));
// 添加环境标签
imageData.environment_tags.forEach(tag => tagSet.add(tag));
imageData.environment_tags.forEach((tag: string) => tagSet.add(tag));
// 添加模特相关标签
imageData.models.forEach(model => {
imageData.models.forEach((model: any) => {
if (model.products) {
model.products.forEach((product: any) => {
if (product.category) {
@@ -471,17 +506,44 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
// 当消息更新时,更新标签列表
useEffect(() => {
const tags = extractAllTags();
setAllTags(tags);
}, [extractAllTags]);
const extractedTags = extractAllTags();
// 合并提取的标签和已选择的标签,确保所有选中的标签都在列表中
const combinedTags = [...new Set([...extractedTags, ...selectedTags])];
setAllTags(combinedTags);
}, [extractAllTags, selectedTags]);
// 切换标签选择状态
const toggleTag = useCallback((tag: string) => {
setSelectedTags(prev =>
prev.includes(tag)
setSelectedTags(prev => {
const newTags = prev.includes(tag)
? prev.filter(t => t !== tag)
: [...prev, tag]
);
: [...prev, tag];
return newTags;
});
}, []);
// 切换标签展开状态
const toggleTagsExpanded = useCallback(() => {
setIsTagsExpanded(prev => !prev);
}, []);
// 显示标签详情气泡
const showTagDetails = useCallback((imageData: any, event: React.MouseEvent) => {
const rect = event.currentTarget.getBoundingClientRect();
setBubblePosition({
x: rect.left + rect.width / 2,
y: rect.top - 10
});
setBubbleTags({
categories: imageData.categories || [],
environment: imageData.environment_tags || []
});
setShowTagBubble(true);
}, []);
// 关闭标签气泡
const closeTagBubble = useCallback(() => {
setShowTagBubble(false);
}, []);
// 清空选中的标签
@@ -534,122 +596,31 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
const handleCopyMessage = useCallback(async (message: ChatMessage) => {
try {
await navigator.clipboard.writeText(message.content);
console.log('📋 已复制到剪贴板:', message.content);
// 如果是AI回复同时在控制台显示详细信息
if (message.type === 'assistant' && message.metadata) {
console.group('📋 复制的AI回复详情');
console.log('💬 回复内容:', message.content);
console.log('⏱️ 响应时间:', message.metadata.responseTime + 'ms');
console.log('🤖 使用模型:', message.metadata.modelUsed);
console.log('🕐 回复时间:', message.timestamp.toLocaleString());
if (message.metadata.sources && message.metadata.sources.length > 0) {
console.log('📚 参考来源:');
message.metadata.sources.forEach((source, index) => {
console.log(` ${index + 1}. ${source.title}`);
if (source.content) {
console.log(` 内容: ${JSON.stringify(source.content, null, 2)}`);
}
if (source.uri) {
console.log(` 链接: ${source.uri}`);
}
});
}
console.groupEnd();
}
console.log(' 已复制到剪贴板');
} catch (err) {
console.error('❌ 复制失败:', err);
}
}, []);
// 在控制台显示消息详情
const handleShowMessageDetails = useCallback((message: ChatMessage) => {
if (message.type === 'assistant') {
console.group('🔍 AI消息详情');
console.log('💬 回复内容:', message.content);
console.log('🕐 时间戳:', message.timestamp.toLocaleString());
console.log('📊 状态:', message.status);
if (message.metadata) {
console.log('⏱️ 响应时间:', message.metadata.responseTime + 'ms');
console.log('🤖 使用模型:', message.metadata.modelUsed);
if (message.metadata.sources && message.metadata.sources.length > 0) {
console.log('📚 参考来源 (' + message.metadata.sources.length + ' 条):');
message.metadata.sources.forEach((source, index) => {
console.log(` 📖 ${index + 1}. ${source.title}`);
if (source.content) {
console.log(` 📄 内容: ${JSON.stringify(source.content, null, 2)}`);
}
if (source.uri) {
console.log(` 🔗 链接: ${source.uri}`);
}
});
}
}
console.groupEnd();
} else {
console.group('👤 用户消息详情');
console.log('💬 消息内容:', message.content);
console.log('🕐 时间戳:', message.timestamp.toLocaleString());
console.log('📊 状态:', message.status);
console.groupEnd();
}
}, []);
return (
<div className={`flex flex-col h-full bg-white rounded-xl shadow-lg border border-gray-200 ${className}`}>
{/* 聊天头部 */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-purple-50 rounded-t-xl">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-md">
<Bot className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-bold text-gray-900">AI </h3>
<p className="text-sm text-gray-600"> RAG </p>
</div>
</div>
<div className="flex items-center gap-2">
{messages.length > 0 && (
<button
onClick={handleClearChat}
className="p-2 text-gray-500 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200"
title="清除对话历史"
>
<Trash2 className="w-4 h-4" />
</button>
)}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-xs text-gray-500">
<MessageCircle className="w-3 h-3" />
<span> {maxMessages} </span>
</div>
<div className="flex items-center gap-1 text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded">
<Terminal className="w-3 h-3" />
<span></span>
</div>
</div>
</div>
</div>
<div className={`flex flex-col h-full bg-white rounded-xl shadow-lg border border-gray-200 ${className} relative`}>
{/* 聊天消息区域 */}
<div
ref={chatContainerRef}
className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50"
className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 pb-32"
style={{ marginBottom: allTags.length > 0 ? '200px' : '120px' }}
>
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="w-16 h-16 bg-gradient-to-br from-pink-100 to-purple-100 rounded-full flex items-center justify-center mb-4">
<Sparkles className="w-8 h-8 text-pink-500" />
<div className="w-20 h-20 bg-gradient-to-br from-pink-100 to-purple-100 rounded-full flex items-center justify-center mb-6">
<Sparkles className="w-10 h-10 text-pink-500" />
</div>
<h4 className="text-lg font-semibold text-gray-700 mb-2">穿</h4>
<p className="text-gray-500 max-w-md">
<p className="text-gray-600 max-w-lg mb-8 text-lg leading-relaxed">
穿
</p>
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-2 max-w-2xl">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl">
{[
'夏日清新穿搭推荐',
'职场女性如何搭配?',
@@ -756,18 +727,11 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
<div className="flex items-center gap-1 ml-2">
<button
onClick={() => handleCopyMessage(message)}
className="p-1 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded transition-colors duration-200"
className="p-1 text-gray-400 hover:text-pink-500 hover:bg-pink-50 rounded transition-colors duration-200"
title="复制消息"
>
<Copy className="w-3 h-3" />
</button>
<button
onClick={() => handleShowMessageDetails(message)}
className="p-1 text-gray-400 hover:text-green-500 hover:bg-green-50 rounded transition-colors duration-200"
title="在控制台显示详情"
>
<Terminal className="w-3 h-3" />
</button>
</div>
</div>
@@ -796,7 +760,9 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
: message.metadata.sources.slice(0, defaultDisplayCount);
return displaySources.map((source, index) =>
renderImageCard(source, index)
<div key={`${index}-${selectedTags.length}-${selectedTags.join(',')}`}>
{renderImageCard(source, index)}
</div>
);
})()}
</div>
@@ -856,64 +822,115 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
</div>
)}
{/* 标签选择区域 */}
{allTags.length > 0 && (
<div className="px-4 pt-4 pb-2 border-t border-gray-200 bg-gradient-to-r from-pink-50 to-purple-50">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Tag className="w-4 h-4 text-pink-500" />
<span className="text-sm font-medium text-gray-700">
穿 ({selectedTags.length} )
</span>
</div>
<div className="flex items-center gap-2">
{selectedTags.length > 0 && (
<button
onClick={() => setInput(generateSearchFromTags())}
className="text-xs text-pink-600 hover:text-pink-800 bg-white hover:bg-pink-50 px-3 py-1 rounded-full transition-all duration-200 shadow-sm hover:shadow-md"
>
</button>
)}
{selectedTags.length > 0 && (
<button
onClick={clearSelectedTags}
className="text-xs text-gray-500 hover:text-gray-700 bg-white hover:bg-gray-100 px-3 py-1 rounded-full transition-all duration-200 shadow-sm"
>
</button>
)}
</div>
</div>
{/* 固定底部输入区域 */}
<div className="absolute bottom-0 left-0 right-0 bg-white border-t border-gray-200 rounded-b-xl">
{/* 标签选择区域 */}
{allTags.length > 0 && (
<div className="border-b border-gray-100 bg-gradient-to-r from-pink-50 to-purple-50">
{/* 标签头部 - 始终显示 */}
<div className="px-4 py-3">
<div className="flex items-center justify-between">
{/* 左侧:标签标题和已选标签 */}
<div className="flex items-center gap-2 flex-1 min-w-0">
<Tag className="w-4 h-4 text-pink-500 flex-shrink-0" />
<span className="text-sm font-medium text-gray-700 flex-shrink-0">
穿 ({selectedTags.length} )
</span>
{/* 标签列表 */}
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
{allTags.map((tag, index) => {
const isSelected = selectedTags.includes(tag);
return (
<button
key={index}
onClick={() => toggleTag(tag)}
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 ${
isSelected
? 'bg-pink-500 text-white shadow-md transform scale-105'
: 'bg-white text-gray-700 hover:bg-pink-100 hover:text-pink-700 hover:shadow-sm border border-gray-200'
}`}
>
<span>{tag}</span>
{isSelected && (
<CheckCircle className="w-3 h-3" />
{/* 已选标签预览 */}
{selectedTags.length > 0 && (
<div className="flex items-center gap-1 ml-2 overflow-hidden">
{selectedTags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="inline-block px-2 py-1 bg-pink-500 text-white text-xs rounded-full flex-shrink-0"
>
{tag}
</span>
))}
{selectedTags.length > 3 && (
<span className="text-xs text-gray-500 flex-shrink-0">+{selectedTags.length - 3}</span>
)}
</div>
)}
</button>
);
})}
</div>
</div>
)}
</div>
{/* 输入区域 */}
<div className="p-4 border-t border-gray-100 bg-white rounded-b-xl">
<div className="flex gap-3">
{/* 右侧:操作按钮和展开按钮 */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* 操作按钮 */}
{selectedTags.length > 0 && (
<>
<button
onClick={() => setInput(generateSearchFromTags())}
className="text-xs text-pink-600 hover:text-pink-800 bg-white hover:bg-pink-50 px-3 py-1 rounded-full transition-all duration-200 shadow-sm hover:shadow-md border border-pink-200"
>
</button>
<button
onClick={clearSelectedTags}
className="text-xs text-gray-500 hover:text-gray-700 bg-white hover:bg-gray-100 px-3 py-1 rounded-full transition-all duration-200 shadow-sm border border-gray-200"
>
</button>
</>
)}
{/* 展开/折叠按钮 */}
<button
onClick={toggleTagsExpanded}
className="flex items-center gap-1 hover:bg-white hover:bg-opacity-50 rounded-lg px-2 py-1 transition-all duration-200"
>
{!isTagsExpanded && (
<div className="bg-pink-500 text-white px-2 py-1 rounded-full text-xs font-medium shadow-md">
</div>
)}
<svg
className={`w-4 h-4 text-pink-500 transition-transform duration-200 ${isTagsExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
</div>
{/* 标签内容 - 可折叠 */}
{isTagsExpanded && (
<div className="px-4 pb-4">
{/* 标签列表 */}
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
{allTags.map((tag, index) => {
const isSelected = selectedTags.includes(tag);
return (
<button
key={index}
onClick={() => toggleTag(tag)}
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 ${
isSelected
? 'bg-pink-500 text-white shadow-md transform scale-105'
: 'bg-white text-gray-700 hover:bg-pink-100 hover:text-pink-700 hover:shadow-sm border border-gray-200'
}`}
>
<span>{tag}</span>
{isSelected && (
<CheckCircle className="w-3 h-3" />
)}
</button>
);
})}
</div>
</div>
)}
</div>
)}
{/* 输入区域 */}
<div className="p-4">
<div className="flex gap-3 items-start">
<div className="flex-1 relative">
<textarea
ref={inputRef}
@@ -940,7 +957,8 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
<button
onClick={handleSendMessage}
disabled={!input.trim() || isLoading}
className="px-4 py-3 bg-pink-500 text-white rounded-xl hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center"
className="px-4 bg-pink-500 text-white rounded-xl hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center flex-shrink-0"
style={{ height: '48px', minHeight: '48px' }}
>
{isLoading ? (
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
@@ -950,14 +968,135 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
</button>
</div>
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
<span> Enter Shift + Enter </span>
<div className="flex items-center gap-1">
<Terminal className="w-3 h-3" />
<span> 📟 </span>
<div className="mt-2 text-center text-xs text-gray-500">
<span> Enter Shift + Enter </span>
</div>
</div>
</div>
{/* 标签详情气泡卡片 */}
{showTagBubble && (
<>
{/* 背景遮罩 */}
<div
className="fixed inset-0 z-40"
onClick={closeTagBubble}
/>
{/* 气泡卡片 */}
<div
className="fixed z-50 bg-white rounded-xl shadow-2xl border border-gray-200 max-w-sm w-80 transform -translate-x-1/2 -translate-y-full"
style={{
left: bubblePosition.x,
top: bubblePosition.y,
}}
>
{/* 气泡箭头 */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2">
<div className="w-0 h-0 border-l-8 border-r-8 border-t-8 border-l-transparent border-r-transparent border-t-white"></div>
<div className="absolute -top-1 left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-8 border-r-8 border-t-8 border-l-transparent border-r-transparent border-t-gray-200"></div>
</div>
{/* 卡片头部 */}
<div className="flex items-center justify-between p-3 border-b border-gray-100 bg-gradient-to-r from-pink-50 to-purple-50 rounded-t-xl">
<div className="flex items-center gap-2">
<Tag className="w-4 h-4 text-pink-500" />
<h3 className="text-sm font-semibold text-gray-800"></h3>
</div>
<button
onClick={closeTagBubble}
className="p-1 hover:bg-gray-100 rounded-full transition-colors duration-200"
>
<XCircle className="w-4 h-4 text-gray-500" />
</button>
</div>
{/* 卡片内容 */}
<div className="p-3 max-h-64 overflow-y-auto">
{/* 分类标签 */}
{bubbleTags.categories.length > 0 && (
<div className="mb-3">
<h4 className="text-xs font-medium text-gray-700 mb-2 flex items-center gap-1">
<Tag className="w-3 h-3 text-blue-500" />
({bubbleTags.categories.length})
</h4>
<div className="flex flex-wrap gap-1">
{bubbleTags.categories.map((category, index) => {
const isSelected = selectedTags.includes(category);
return (
<button
key={index}
onClick={() => toggleTag(category)}
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full transition-all duration-200 border ${
isSelected
? 'bg-pink-500 text-white shadow-md border-pink-600'
: 'bg-blue-50 text-blue-700 hover:bg-pink-100 hover:text-pink-700 border-blue-200 hover:border-pink-300'
}`}
>
<Tag className="w-2 h-2" />
{category}
</button>
);
})}
</div>
</div>
)}
{/* 环境标签 */}
{bubbleTags.environment.length > 0 && (
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2 flex items-center gap-1">
<MapPin className="w-3 h-3 text-green-500" />
({bubbleTags.environment.length})
</h4>
<div className="flex flex-wrap gap-1">
{bubbleTags.environment.map((tag, index) => {
const isSelected = selectedTags.includes(tag);
return (
<button
key={index}
onClick={() => toggleTag(tag)}
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full transition-all duration-200 border ${
isSelected
? 'bg-pink-500 text-white shadow-md border-pink-600'
: 'bg-green-50 text-green-700 hover:bg-pink-100 hover:text-pink-700 border-green-200 hover:border-pink-300'
}`}
>
<MapPin className="w-2 h-2" />
{tag}
</button>
);
})}
</div>
</div>
)}
{/* 空状态 */}
{bubbleTags.categories.length === 0 && bubbleTags.environment.length === 0 && (
<div className="text-center py-4 text-gray-500">
<Tag className="w-8 h-8 mx-auto mb-1 text-gray-300" />
<p className="text-xs"></p>
</div>
)}
</div>
{/* 卡片底部 */}
<div className="px-3 py-2 border-t border-gray-100 bg-gray-50 rounded-b-xl">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600">
{selectedTags.length}
</span>
<button
onClick={closeTagBubble}
className="px-3 py-1 bg-pink-500 text-white rounded-md hover:bg-pink-600 transition-colors duration-200 text-xs"
>
</button>
</div>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -46,7 +46,7 @@ const Navigation: React.FC = () => {
},
{
name: '服装搭配',
href: '/outfit-match',
href: '/fashion-chat',
icon: SparklesIcon,
description: 'AI智能服装搭配推荐'
},

View File

@@ -63,16 +63,8 @@ const ChatTool: React.FC = () => {
return (
<div className="h-full flex flex-col animate-fade-in">
{/* 页面头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-white">
<div className="flex items-center gap-4">
<button
onClick={handleBack}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-all duration-200"
title="返回工具列表"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="page-header flex items-center justify-between p-6 border-b border-gray-200 bg-white">
<div className=" flex items-center gap-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<MessageCircle className="w-6 h-6 text-white" />