chore(history): import initial web product snapshot
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Check,
|
||||
CheckCheck,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
Copy,
|
||||
Reply,
|
||||
Forward,
|
||||
StickyNote,
|
||||
Star,
|
||||
ListTodo,
|
||||
BookOpen,
|
||||
MoreHorizontal,
|
||||
} from 'lucide-react'
|
||||
import type { Message } from '@/types'
|
||||
import { cn } from '@/utils/cn'
|
||||
import { toast } from '@/app/store/toast'
|
||||
import { formatTime } from '@/utils/format'
|
||||
import { languageName } from '@/config/languages'
|
||||
import { Dropdown } from '@/components/ui/dropdown'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
export type ViewMode = 'original' | 'translated' | 'bilingual'
|
||||
|
||||
export function MessageBubble({
|
||||
msg,
|
||||
viewMode,
|
||||
onQuote,
|
||||
onMarkImportant,
|
||||
onConvertToTask,
|
||||
onAddToKnowledge,
|
||||
onAddNote,
|
||||
}: {
|
||||
msg: Message
|
||||
viewMode: ViewMode
|
||||
onQuote: () => void
|
||||
onMarkImportant: () => void
|
||||
onConvertToTask: () => void
|
||||
onAddToKnowledge: () => void
|
||||
onAddNote: () => void
|
||||
}) {
|
||||
const [showActions, setShowActions] = useState(false)
|
||||
const isMe = msg.sender === 'agent'
|
||||
const isSystem = msg.sender === 'system'
|
||||
const hasTranslation = msg.translatedText && msg.translatedText !== msg.originalText
|
||||
|
||||
if (isSystem) {
|
||||
return (
|
||||
<div className="flex justify-center py-2">
|
||||
<span className="rounded-full bg-amber-50 px-3 py-1 text-xs text-amber-700">{msg.originalText}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (viewMode === 'bilingual' && hasTranslation) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-[10px] text-muted-foreground">{languageName(msg.originalLang)}</span>
|
||||
<p className="mt-0.5 whitespace-pre-wrap break-words text-sm">{msg.originalText}</p>
|
||||
</div>
|
||||
<div className="border-t border-border/40 pt-2">
|
||||
<span className="text-[10px] text-muted-foreground">中文释义</span>
|
||||
<p className="mt-0.5 whitespace-pre-wrap break-words text-sm text-muted-foreground">{msg.translatedText}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (viewMode === 'translated' && msg.translatedText) {
|
||||
return (
|
||||
<div>
|
||||
<span className="text-[10px] text-muted-foreground">{languageName(msg.originalLang)} → 中文</span>
|
||||
<p className="mt-0.5 whitespace-pre-wrap break-words text-sm">{msg.translatedText}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span className="text-[10px] text-muted-foreground">{languageName(msg.originalLang)}</span>
|
||||
<p className="mt-0.5 whitespace-pre-wrap break-words text-sm">{msg.originalText}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const statusIcon = () => {
|
||||
switch (msg.status) {
|
||||
case 'sending':
|
||||
return <Clock className="h-3 w-3 text-muted-foreground animate-pulse" />
|
||||
case 'sent':
|
||||
return <Check className="h-3 w-3 text-muted-foreground" />
|
||||
case 'delivered':
|
||||
return <CheckCheck className="h-3 w-3 text-muted-foreground" />
|
||||
case 'read':
|
||||
return <CheckCheck className="h-3 w-3 text-primary" />
|
||||
case 'failed':
|
||||
return <AlertCircle className="h-3 w-3 text-red-500" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const moreActions = [
|
||||
{ key: 'copy', label: '复制', icon: Copy, onSelect: () => { navigator.clipboard.writeText(msg.originalText); toast.success('已复制') } },
|
||||
{ key: 'reply', label: '引用回复', icon: Reply, onSelect: onQuote },
|
||||
{ key: 'forward', label: '转发', icon: Forward, onSelect: () => toast.info('转发功能:选择目标会话(演示)') },
|
||||
{ key: 'note', label: '添加内部备注', icon: StickyNote, onSelect: onAddNote },
|
||||
{ key: 'important', label: msg.isImportant ? '取消重要' : '标记重要', icon: Star, onSelect: onMarkImportant },
|
||||
{ key: 'task', label: '转为待办', icon: ListTodo, onSelect: onConvertToTask },
|
||||
{ key: 'knowledge', label: '添加到知识库', icon: BookOpen, onSelect: onAddToKnowledge },
|
||||
]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex gap-2 px-4 py-2.5', isMe ? 'justify-end' : 'justify-start')}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
>
|
||||
<div className={cn('max-w-[70%] min-w-0', isMe ? 'order-2' : 'order-1')}>
|
||||
{/* 消息气泡 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded-lg px-3 py-2.5',
|
||||
isMe ? 'bg-primary-50' : 'bg-card border border-border',
|
||||
msg.isImportant && 'ring-1 ring-amber-400',
|
||||
)}
|
||||
>
|
||||
{renderContent()}
|
||||
{msg.attachmentName && msg.type === 'image' && (
|
||||
<div className="mt-2 flex h-24 items-center justify-center rounded bg-muted text-xs text-muted-foreground">
|
||||
📷 {msg.attachmentName}
|
||||
</div>
|
||||
)}
|
||||
{msg.attachmentName && msg.type === 'file' && (
|
||||
<div className="mt-1.5 rounded bg-muted px-2 py-1 text-xs text-muted-foreground">
|
||||
📎 {msg.attachmentName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底栏:时间 + 状态 */}
|
||||
<div className={cn('mt-0.5 flex items-center gap-1.5 text-[10px] text-muted-foreground', isMe ? 'justify-end' : 'justify-start')}>
|
||||
<span>{formatTime(msg.createdAt)}</span>
|
||||
{isMe && statusIcon()}
|
||||
{msg.isImportant && <Star className="h-2.5 w-2.5 fill-amber-400 text-amber-400" />}
|
||||
{msg.internalNote && <StickyNote className="h-2.5 w-2.5 text-blue-500" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 悬停操作按钮 */}
|
||||
<div className={cn('flex items-center', isMe ? 'order-1' : 'order-2')}>
|
||||
{showActions && (
|
||||
<Dropdown
|
||||
trigger={
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted">
|
||||
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
}
|
||||
items={moreActions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user