chore(history): import initial web product snapshot

This commit is contained in:
wmq
2026-07-25 00:30:00 +08:00
commit cf0eed46c3
131 changed files with 15693 additions and 0 deletions
+166
View File
@@ -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>
)
}