106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import React, { useMemo } from 'react'
|
||
import { Copy } from 'lucide-react'
|
||
import { cn } from '@/utils/cn'
|
||
|
||
export interface MessageFooterProps {
|
||
shouldShowTimestamp: boolean
|
||
shouldShowCopyButton: boolean
|
||
timestamp?: string | number
|
||
isUserInput: boolean
|
||
isSummary: boolean
|
||
showSystemAttachments: boolean
|
||
showUserFile: boolean
|
||
onCopyMessage: () => void
|
||
}
|
||
|
||
function parseTimestamp(timestamp: string | number): Date {
|
||
if (typeof timestamp === 'number') return new Date(timestamp)
|
||
const num = Number(timestamp)
|
||
if (!isNaN(num)) return new Date(num)
|
||
// 截断超过3位的小数秒(Python 微秒精度 → JS 毫秒精度)
|
||
const normalized = timestamp.replace(/(\.\d{3})\d+/, '$1')
|
||
return new Date(normalized)
|
||
}
|
||
|
||
function formatTimestamp(timestamp: string | number): string {
|
||
const date = parseTimestamp(timestamp)
|
||
|
||
const now = new Date()
|
||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
const dateStart = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||
const diffDays = Math.round(
|
||
(todayStart.getTime() - dateStart.getTime()) / (1000 * 60 * 60 * 24),
|
||
)
|
||
|
||
const hhmm = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
|
||
|
||
if (diffDays === 0) {
|
||
return hhmm
|
||
} else if (diffDays >= 1) {
|
||
return `${date.getMonth() + 1}/${date.getDate()} ${hhmm}`
|
||
} else if (now.getFullYear() === date.getFullYear()) {
|
||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||
} else {
|
||
return `${date.getFullYear()}`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 消息底部 - 显示时间戳和复制按钮,对应 next-agent MessageFooter
|
||
*/
|
||
export const MessageFooter: React.FC<MessageFooterProps> = ({
|
||
shouldShowTimestamp,
|
||
shouldShowCopyButton,
|
||
timestamp,
|
||
isUserInput,
|
||
isSummary,
|
||
showSystemAttachments,
|
||
showUserFile,
|
||
onCopyMessage,
|
||
}) => {
|
||
// hooks 必须在任何 return 之前调用
|
||
const displayText = useMemo(
|
||
() => (timestamp != null ? formatTimestamp(timestamp) : ''),
|
||
[timestamp],
|
||
)
|
||
|
||
const fullTime = useMemo(() => {
|
||
if (timestamp == null) return ''
|
||
return parseTimestamp(timestamp).toLocaleString()
|
||
}, [timestamp])
|
||
|
||
if (!shouldShowTimestamp) return null
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'opacity-0 group-hover:opacity-100 flex items-center bottom-1 gap-3',
|
||
{
|
||
'right-0 absolute': isUserInput,
|
||
'flex-row-reverse': isSummary,
|
||
'mt-2.5': isSummary && showSystemAttachments,
|
||
'px-3': showUserFile,
|
||
},
|
||
)}
|
||
>
|
||
<span
|
||
className="text-xs text-muted-foreground whitespace-nowrap"
|
||
title={fullTime}
|
||
>
|
||
{displayText}
|
||
</span>
|
||
{shouldShowCopyButton && (
|
||
<button
|
||
className="w-6 h-6 rounded flex items-center justify-center hover:bg-muted transition-colors"
|
||
onClick={onCopyMessage}
|
||
type="button"
|
||
>
|
||
<Copy className="w-3.5 h-3.5 text-muted-foreground cursor-pointer" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default React.memo(MessageFooter)
|