feat: 完善聊天界面功能和用户体验
界面优化: - 修复页面头部信息重复问题,统一为时尚穿搭顾问 - 优化欢迎界面布局,移除重复标题 - 调整标签激活状态样式,使其更加subtle和优雅 标签系统增强: - 实现卡片标签与底部工具栏标签完美同步 - 修复标签选中状态的实时更新问题 - 添加气泡卡片展示所有标签详情 - 优化标签折叠功能,默认隐藏,一行布局 功能完善: - 修复输入框和发送按钮对齐问题 - 优化聊天输入框固定到底部 - 将弹框改为更优雅的气泡卡片消息 - 生成搜索和清空按钮移到外层,始终可见 导航优化: - 服装搭配导航直接链接到AI智能聊天 - 配置独立路由避免与便捷工具冲突 - 移除原有的服装搭配页面和路由 用户体验: - 标签选中状态双向同步更新 - 气泡卡片支持标签选择和实时反馈 - 优化加载状态显示和错误处理 - 完善的视觉反馈和交互动画
This commit is contained in:
@@ -15,7 +15,7 @@ import JsonParserTool from './pages/tools/JsonParserTool';
|
|||||||
import DebugPanelTool from './pages/tools/DebugPanelTool';
|
import DebugPanelTool from './pages/tools/DebugPanelTool';
|
||||||
import ChatTool from './pages/tools/ChatTool';
|
import ChatTool from './pages/tools/ChatTool';
|
||||||
import ChatTestPage from './pages/tools/ChatTestPage';
|
import ChatTestPage from './pages/tools/ChatTestPage';
|
||||||
import OutfitMatch from './pages/OutfitMatch';
|
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './components/Navigation';
|
||||||
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
|
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
|
||||||
import { useProjectStore } from './store/projectStore';
|
import { useProjectStore } from './store/projectStore';
|
||||||
@@ -91,7 +91,8 @@ function App() {
|
|||||||
<Route path="/ai-classification-settings" element={<AiClassificationSettings />} />
|
<Route path="/ai-classification-settings" element={<AiClassificationSettings />} />
|
||||||
<Route path="/templates" element={<TemplateManagement />} />
|
<Route path="/templates" element={<TemplateManagement />} />
|
||||||
<Route path="/material-model-binding" element={<MaterialModelBinding />} />
|
<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" element={<Tools />} />
|
||||||
<Route path="/tools/data-cleaning" element={<DataCleaningTool />} />
|
<Route path="/tools/data-cleaning" element={<DataCleaningTool />} />
|
||||||
<Route path="/tools/json-parser" element={<JsonParserTool />} />
|
<Route path="/tools/json-parser" element={<JsonParserTool />} />
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Terminal,
|
|
||||||
Copy,
|
Copy,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -19,7 +18,7 @@ import {
|
|||||||
MapPin
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { queryRagGrounding } from '../services/ragGroundingService';
|
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 [showAnswers, setShowAnswers] = useState<Record<string, boolean>>({});
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const [allTags, setAllTags] = 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 inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -192,17 +195,37 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
{/* 分类标签 */}
|
{/* 分类标签 */}
|
||||||
{imageData.categories.length > 0 && (
|
{imageData.categories.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{imageData.categories.slice(0, 4).map((category: string, catIndex: number) => (
|
{imageData.categories.slice(0, 4).map((category: string, catIndex: number) => {
|
||||||
<span
|
const isSelected = selectedTags.includes(category);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
key={catIndex}
|
key={catIndex}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full"
|
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" />
|
<Tag className="w-2 h-2" />
|
||||||
{category}
|
{category}
|
||||||
</span>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{imageData.categories.length > 4 && (
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -210,15 +233,27 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
{/* 环境标签 */}
|
{/* 环境标签 */}
|
||||||
{imageData.environment_tags.length > 0 && (
|
{imageData.environment_tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{imageData.environment_tags.slice(0, 3).map((tag: string, tagIndex: number) => (
|
{imageData.environment_tags.slice(0, 3).map((tag: string, tagIndex: number) => {
|
||||||
<span
|
const isSelected = selectedTags.includes(tag);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
key={tagIndex}
|
key={tagIndex}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full"
|
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" />
|
<MapPin className="w-2 h-2" />
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -253,7 +288,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [parseImageContent]);
|
}, [parseImageContent, selectedTags]);
|
||||||
|
|
||||||
// 生成消息ID
|
// 生成消息ID
|
||||||
const generateMessageId = useCallback(() => {
|
const generateMessageId = useCallback(() => {
|
||||||
@@ -448,11 +483,11 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
const imageData = parseImageContent(source.content);
|
const imageData = parseImageContent(source.content);
|
||||||
if (imageData) {
|
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) {
|
if (model.products) {
|
||||||
model.products.forEach((product: any) => {
|
model.products.forEach((product: any) => {
|
||||||
if (product.category) {
|
if (product.category) {
|
||||||
@@ -471,17 +506,44 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
|
|
||||||
// 当消息更新时,更新标签列表
|
// 当消息更新时,更新标签列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tags = extractAllTags();
|
const extractedTags = extractAllTags();
|
||||||
setAllTags(tags);
|
// 合并提取的标签和已选择的标签,确保所有选中的标签都在列表中
|
||||||
}, [extractAllTags]);
|
const combinedTags = [...new Set([...extractedTags, ...selectedTags])];
|
||||||
|
setAllTags(combinedTags);
|
||||||
|
}, [extractAllTags, selectedTags]);
|
||||||
|
|
||||||
// 切换标签选择状态
|
// 切换标签选择状态
|
||||||
const toggleTag = useCallback((tag: string) => {
|
const toggleTag = useCallback((tag: string) => {
|
||||||
setSelectedTags(prev =>
|
setSelectedTags(prev => {
|
||||||
prev.includes(tag)
|
const newTags = prev.includes(tag)
|
||||||
? prev.filter(t => t !== 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) => {
|
const handleCopyMessage = useCallback(async (message: ChatMessage) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(message.content);
|
await navigator.clipboard.writeText(message.content);
|
||||||
console.log('📋 已复制到剪贴板:', message.content);
|
console.log('✅ 已复制到剪贴板');
|
||||||
|
|
||||||
// 如果是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();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ 复制失败:', 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 (
|
return (
|
||||||
<div className={`flex flex-col h-full bg-white rounded-xl shadow-lg border border-gray-200 ${className}`}>
|
<div className={`flex flex-col h-full bg-white rounded-xl shadow-lg border border-gray-200 ${className} relative`}>
|
||||||
{/* 聊天头部 */}
|
|
||||||
<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
|
<div
|
||||||
ref={chatContainerRef}
|
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 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
<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">
|
<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-8 h-8 text-pink-500" />
|
<Sparkles className="w-10 h-10 text-pink-500" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-lg font-semibold text-gray-700 mb-2">时尚穿搭顾问</h4>
|
<p className="text-gray-600 max-w-lg mb-8 text-lg leading-relaxed">
|
||||||
<p className="text-gray-500 max-w-md">
|
|
||||||
我是您的专属时尚顾问,基于丰富的女装穿搭知识库,为您提供个性化的搭配建议和时尚指导。
|
我是您的专属时尚顾问,基于丰富的女装穿搭知识库,为您提供个性化的搭配建议和时尚指导。
|
||||||
</p>
|
</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">
|
<div className="flex items-center gap-1 ml-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCopyMessage(message)}
|
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="复制消息"
|
title="复制消息"
|
||||||
>
|
>
|
||||||
<Copy className="w-3 h-3" />
|
<Copy className="w-3 h-3" />
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -796,7 +760,9 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
: message.metadata.sources.slice(0, defaultDisplayCount);
|
: message.metadata.sources.slice(0, defaultDisplayCount);
|
||||||
|
|
||||||
return displaySources.map((source, index) =>
|
return displaySources.map((source, index) =>
|
||||||
renderImageCard(source, index)
|
<div key={`${index}-${selectedTags.length}-${selectedTags.join(',')}`}>
|
||||||
|
{renderImageCard(source, index)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
@@ -856,36 +822,85 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 固定底部输入区域 */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-white border-t border-gray-200 rounded-b-xl">
|
||||||
{/* 标签选择区域 */}
|
{/* 标签选择区域 */}
|
||||||
{allTags.length > 0 && (
|
{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="border-b border-gray-100 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">
|
<div className="px-4 py-3">
|
||||||
<Tag className="w-4 h-4 text-pink-500" />
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium text-gray-700">
|
{/* 左侧:标签标题和已选标签 */}
|
||||||
|
<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} 已选)
|
穿搭标签 ({selectedTags.length} 已选)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
{/* 已选标签预览 */}
|
||||||
{selectedTags.length > 0 && (
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:操作按钮和展开按钮 */}
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
{selectedTags.length > 0 && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setInput(generateSearchFromTags())}
|
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"
|
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>
|
||||||
)}
|
|
||||||
{selectedTags.length > 0 && (
|
|
||||||
<button
|
<button
|
||||||
onClick={clearSelectedTags}
|
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"
|
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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 展开/折叠按钮 */}
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 标签内容 - 可折叠 */}
|
||||||
|
{isTagsExpanded && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
{/* 标签列表 */}
|
{/* 标签列表 */}
|
||||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||||
{allTags.map((tag, index) => {
|
{allTags.map((tag, index) => {
|
||||||
@@ -910,10 +925,12 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 输入区域 */}
|
{/* 输入区域 */}
|
||||||
<div className="p-4 border-t border-gray-100 bg-white rounded-b-xl">
|
<div className="p-4">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3 items-start">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -940,7 +957,8 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
disabled={!input.trim() || isLoading}
|
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 ? (
|
{isLoading ? (
|
||||||
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
<div className="mt-2 text-center text-xs text-gray-500">
|
||||||
<span>按 Enter 发送,Shift + Enter 换行</span>
|
<span>按 Enter 发送,Shift + Enter 换行</span>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Terminal className="w-3 h-3" />
|
|
||||||
<span>点击消息旁的 📟 按钮查看详细日志</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const Navigation: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '服装搭配',
|
name: '服装搭配',
|
||||||
href: '/outfit-match',
|
href: '/fashion-chat',
|
||||||
icon: SparklesIcon,
|
icon: SparklesIcon,
|
||||||
description: 'AI智能服装搭配推荐'
|
description: 'AI智能服装搭配推荐'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,16 +63,8 @@ const ChatTool: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col animate-fade-in">
|
<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="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-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="flex items-center gap-3">
|
<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">
|
<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" />
|
<MessageCircle className="w-6 h-6 text-white" />
|
||||||
|
|||||||
Reference in New Issue
Block a user