@@ -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 t ags = extractAllTags ( ) ;
setAllTags ( tags ) ;
} , [ extractAll Tags ] ) ;
const extractedT ags = extractAllTags ( ) ;
// 合并提取的标签和已选择的标签,确保所有选中的标签都在列表中
const combinedTags = [ . . . new Set ( [ . . . extractedTags , . . . selected Tags] ) ] ;
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 >
) ;
} ;