This commit is contained in:
cc committed 2026-06-03 19:13:07 +08:00
1 parent f3b0cf5d3a
commit 54016abac5
382 files changed
+2 -188190

No files matched your search

-271
View File
@@ -1,271 +0,0 @@
import React from 'react'
import {
Aperture,
BarChart3,
Calendar,
Download,
Image as ImageIcon,
Info,
Loader2,
Mic,
Newspaper,
RefreshCw,
Search,
Sparkles,
Users
} from 'lucide-react'
import { Avatar } from '../../components/Avatar'
import type { ChatSession } from '../../types/models'
import type { BatchVoiceTaskType } from '../../stores/batchTranscribeStore'
export interface ChatHeaderProps {
session: ChatSession
isGroupChat: boolean
standaloneSessionWindow: boolean
showGroupMembersPanel: boolean
showGroupSummaryPanel: boolean
showJumpPopover: boolean
showInSessionSearch: boolean
showDetailPanel: boolean
aiGroupSummaryEnabled: boolean
shouldHideStandaloneDetailButton: boolean
isPrivateSnsSupported: boolean
isExportActionBusy: boolean
isCurrentSessionExporting: boolean
isPreparingExportDialog: boolean
isBatchTranscribing: boolean
runningBatchVoiceTaskType?: BatchVoiceTaskType
isBatchDecrypting: boolean
isTriggeringSessionInsight: boolean
isRefreshingMessages: boolean
isLoadingMessages: boolean
currentSessionId?: string | null
jumpCalendarWrapRef: React.RefObject<HTMLDivElement | null>
onTriggerSessionInsight: () => void
onToggleGroupSummaryPanel: () => void
onGroupAnalytics: () => void
onToggleGroupMembersPanel: () => void
onExportCurrentSession: () => void
onOpenSnsTimeline: () => void
onBatchTranscribe: () => void
onBatchDecrypt: () => void
onToggleJumpPopover: () => void
onToggleInSessionSearch: () => void
onRefreshMessages: () => void
onToggleDetailPanel: () => void
}
function ChatHeader({
session,
isGroupChat,
standaloneSessionWindow,
showGroupMembersPanel,
showGroupSummaryPanel,
showJumpPopover,
showInSessionSearch,
showDetailPanel,
aiGroupSummaryEnabled,
shouldHideStandaloneDetailButton,
isPrivateSnsSupported,
isExportActionBusy,
isCurrentSessionExporting,
isPreparingExportDialog,
isBatchTranscribing,
runningBatchVoiceTaskType,
isBatchDecrypting,
isTriggeringSessionInsight,
isRefreshingMessages,
isLoadingMessages,
currentSessionId,
jumpCalendarWrapRef,
onTriggerSessionInsight,
onToggleGroupSummaryPanel,
onGroupAnalytics,
onToggleGroupMembersPanel,
onExportCurrentSession,
onOpenSnsTimeline,
onBatchTranscribe,
onBatchDecrypt,
onToggleJumpPopover,
onToggleInSessionSearch,
onRefreshMessages,
onToggleDetailPanel
}: ChatHeaderProps) {
const sessionName = session.displayName || session.username
const exportTitle = isCurrentSessionExporting
? '导出中'
: isPreparingExportDialog
? '正在准备导出模块'
: '导出当前会话'
const batchVoiceTitle = isBatchTranscribing
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度`
: '批量语音处理'
return (
<div className="message-header">
<Avatar
src={session.avatarUrl}
name={sessionName}
size={40}
className={isGroupChat ? 'group session-avatar' : 'session-avatar'}
/>
<div className="header-info">
<h3>{sessionName}</h3>
{isGroupChat && <div className="header-subtitle">群聊</div>}
</div>
<div className="header-actions">
<button
className={`icon-btn session-insight-btn${isTriggeringSessionInsight ? ' triggering' : ''}`}
onClick={onTriggerSessionInsight}
disabled={!currentSessionId || isTriggeringSessionInsight}
title={isTriggeringSessionInsight ? '正在生成 AI 见解' : '立即触发当前聊天 AI 见解'}
aria-label="立即触发当前聊天 AI 见解"
>
{isTriggeringSessionInsight ? <Loader2 size={18} className="spin" /> : <Sparkles size={18} />}
</button>
{isGroupChat && aiGroupSummaryEnabled && (
<button
className={`icon-btn group-summary-btn ${showGroupSummaryPanel ? 'active' : ''}`}
onClick={onToggleGroupSummaryPanel}
disabled={!currentSessionId}
title="AI 群聊总结"
aria-label="AI 群聊总结"
>
<Newspaper size={18} />
</button>
)}
{!standaloneSessionWindow && isGroupChat && (
<button className="icon-btn group-analytics-btn" onClick={onGroupAnalytics} title="群聊分析">
<BarChart3 size={18} />
</button>
)}
{isGroupChat && (
<button
className={`icon-btn group-members-btn ${showGroupMembersPanel ? 'active' : ''}`}
onClick={onToggleGroupMembersPanel}
title="群成员"
>
<Users size={18} />
</button>
)}
{!standaloneSessionWindow && (
<button
className={`icon-btn export-session-btn${isExportActionBusy ? ' exporting' : ''}`}
onClick={onExportCurrentSession}
disabled={!currentSessionId || isExportActionBusy}
title={exportTitle}
>
{isExportActionBusy ? <Loader2 size={18} className="spin" /> : <Download size={18} />}
</button>
)}
{!standaloneSessionWindow && isPrivateSnsSupported && (
<button
className="icon-btn chat-sns-timeline-btn"
onClick={onOpenSnsTimeline}
disabled={!currentSessionId}
title="查看朋友圈"
>
<Aperture size={18} />
</button>
)}
{!standaloneSessionWindow && (
<button
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
onClick={onBatchTranscribe}
disabled={!currentSessionId}
title={batchVoiceTitle}
>
{isBatchTranscribing ? <Loader2 size={18} className="spin" /> : <Mic size={18} />}
</button>
)}
{!standaloneSessionWindow && (
<button
className={`icon-btn batch-decrypt-btn${isBatchDecrypting ? ' transcribing' : ''}`}
onClick={onBatchDecrypt}
disabled={!currentSessionId}
title={isBatchDecrypting ? '批量解密中' : '批量解密图片'}
>
{isBatchDecrypting ? <Loader2 size={18} className="spin" /> : <ImageIcon size={18} />}
</button>
)}
<div className="jump-calendar-anchor" ref={jumpCalendarWrapRef}>
<button
className={`icon-btn jump-to-time-btn ${showJumpPopover ? 'active' : ''}`}
onClick={onToggleJumpPopover}
title="跳转到指定时间"
>
<Calendar size={18} />
</button>
</div>
<button
className={`icon-btn in-session-search-btn ${showInSessionSearch ? 'active' : ''}`}
onClick={onToggleInSessionSearch}
disabled={!currentSessionId}
title="搜索会话消息"
>
<Search size={18} />
</button>
<button
className="icon-btn refresh-messages-btn"
onClick={onRefreshMessages}
disabled={isRefreshingMessages || isLoadingMessages}
title="刷新消息"
>
<RefreshCw size={18} className={isRefreshingMessages ? 'spin' : ''} />
</button>
{!shouldHideStandaloneDetailButton && (
<button
className={`icon-btn detail-btn ${showDetailPanel ? 'active' : ''}`}
onClick={onToggleDetailPanel}
title="会话详情"
>
<Info size={18} />
</button>
)}
</div>
</div>
)
}
function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
return (
prev.session.username === next.session.username &&
prev.session.displayName === next.session.displayName &&
prev.session.avatarUrl === next.session.avatarUrl &&
prev.isGroupChat === next.isGroupChat &&
prev.standaloneSessionWindow === next.standaloneSessionWindow &&
prev.showGroupMembersPanel === next.showGroupMembersPanel &&
prev.showGroupSummaryPanel === next.showGroupSummaryPanel &&
prev.showJumpPopover === next.showJumpPopover &&
prev.showInSessionSearch === next.showInSessionSearch &&
prev.showDetailPanel === next.showDetailPanel &&
prev.aiGroupSummaryEnabled === next.aiGroupSummaryEnabled &&
prev.shouldHideStandaloneDetailButton === next.shouldHideStandaloneDetailButton &&
prev.isPrivateSnsSupported === next.isPrivateSnsSupported &&
prev.isExportActionBusy === next.isExportActionBusy &&
prev.isCurrentSessionExporting === next.isCurrentSessionExporting &&
prev.isPreparingExportDialog === next.isPreparingExportDialog &&
prev.isBatchTranscribing === next.isBatchTranscribing &&
prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType &&
prev.isBatchDecrypting === next.isBatchDecrypting &&
prev.isTriggeringSessionInsight === next.isTriggeringSessionInsight &&
prev.isRefreshingMessages === next.isRefreshingMessages &&
prev.isLoadingMessages === next.isLoadingMessages &&
prev.currentSessionId === next.currentSessionId &&
prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef &&
prev.onTriggerSessionInsight === next.onTriggerSessionInsight &&
prev.onToggleGroupSummaryPanel === next.onToggleGroupSummaryPanel &&
prev.onGroupAnalytics === next.onGroupAnalytics &&
prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel &&
prev.onExportCurrentSession === next.onExportCurrentSession &&
prev.onOpenSnsTimeline === next.onOpenSnsTimeline &&
prev.onBatchTranscribe === next.onBatchTranscribe &&
prev.onBatchDecrypt === next.onBatchDecrypt &&
prev.onToggleJumpPopover === next.onToggleJumpPopover &&
prev.onToggleInSessionSearch === next.onToggleInSessionSearch &&
prev.onRefreshMessages === next.onRefreshMessages &&
prev.onToggleDetailPanel === next.onToggleDetailPanel
)
}
export default React.memo(ChatHeader, areEqual)
-150
View File
@@ -1,150 +0,0 @@
import React from 'react'
import { Check } from 'lucide-react'
import { Avatar } from '../../components/Avatar'
import type { ChatSession, Message } from '../../types/models'
export interface ChatMessageBubbleProps {
message: Message
messageKey: string
session: ChatSession
showTime?: boolean
timeText?: string
isSent: boolean
isSystem: boolean
isEmoji?: boolean
isImage?: boolean
isVideo?: boolean
isVoice?: boolean
emojiHasAsset?: boolean
emojiError?: boolean
avatarUrl?: string
isGroupChat?: boolean
resolvedSenderName?: string
isSelectionMode?: boolean
isSelected?: boolean
onContextMenu?: (event: React.MouseEvent, message: Message) => void
onToggleSelection?: (messageKey: string, isShiftKey?: boolean) => void
actionNode?: React.ReactNode
children: React.ReactNode
portal?: React.ReactNode
}
function SelectionCheckbox({ checked, side }: { checked?: boolean; side: 'left' | 'right' }) {
return (
<div className={`chat-selection-checkbox ${side} ${checked ? 'checked' : ''}`}>
{checked && <Check size={14} strokeWidth={3} />}
</div>
)
}
function ChatMessageBubble({
message,
messageKey,
session,
showTime,
timeText,
isSent,
isSystem,
isEmoji,
isImage,
isVideo,
isVoice,
emojiHasAsset,
emojiError,
avatarUrl,
isGroupChat,
resolvedSenderName,
isSelectionMode,
isSelected,
onContextMenu,
onToggleSelection,
actionNode,
children,
portal
}: ChatMessageBubbleProps) {
const bubbleClass = isSystem ? 'system' : (isSent ? 'sent' : 'received')
const avatarName = !isSent
? (isGroupChat ? (resolvedSenderName || '?') : (session.displayName || session.username))
: '我'
return (
<>
{showTime && timeText && (
<div className="time-divider">
<span>{timeText}</span>
</div>
)}
<div
className={`message-wrapper-with-selection ${isSelectionMode ? 'selectable' : ''}`}
data-sent={isSent ? 'true' : 'false'}
onClick={(event) => {
if (!isSelectionMode) return
event.stopPropagation()
onToggleSelection?.(messageKey, event.shiftKey)
}}
>
{isSelectionMode && !isSent && <SelectionCheckbox checked={isSelected} side="left" />}
<div
className={`message-bubble ${bubbleClass} ${isEmoji && emojiHasAsset && !emojiError ? 'emoji' : ''} ${isImage ? 'image' : ''} ${isVideo ? 'video' : ''} ${isVoice ? 'voice' : ''}`}
onContextMenu={(event) => onContextMenu?.(event, message)}
>
<div className="bubble-avatar">
<Avatar src={avatarUrl} name={avatarName} size={36} className="bubble-avatar" />
</div>
<div className="bubble-body">
{isGroupChat && !isSent && (
<div className="sender-line">
<div className="sender-name">
{resolvedSenderName || '群成员'}
</div>
{actionNode}
</div>
)}
{children}
</div>
{!isGroupChat && !isSent && actionNode ? (
<div className="message-action-inline">
{actionNode}
</div>
) : null}
</div>
{isSelectionMode && isSent && <SelectionCheckbox checked={isSelected} side="right" />}
{portal}
</div>
</>
)
}
function areEqual(prev: ChatMessageBubbleProps, next: ChatMessageBubbleProps) {
return (
prev.message === next.message &&
prev.messageKey === next.messageKey &&
prev.session.username === next.session.username &&
prev.session.displayName === next.session.displayName &&
prev.session.avatarUrl === next.session.avatarUrl &&
prev.showTime === next.showTime &&
prev.timeText === next.timeText &&
prev.isSent === next.isSent &&
prev.isSystem === next.isSystem &&
prev.isEmoji === next.isEmoji &&
prev.isImage === next.isImage &&
prev.isVideo === next.isVideo &&
prev.isVoice === next.isVoice &&
prev.emojiHasAsset === next.emojiHasAsset &&
prev.emojiError === next.emojiError &&
prev.avatarUrl === next.avatarUrl &&
prev.isGroupChat === next.isGroupChat &&
prev.resolvedSenderName === next.resolvedSenderName &&
prev.isSelectionMode === next.isSelectionMode &&
prev.isSelected === next.isSelected &&
prev.onContextMenu === next.onContextMenu &&
prev.onToggleSelection === next.onToggleSelection &&
prev.actionNode === next.actionNode &&
prev.children === next.children &&
prev.portal === next.portal
)
}
export default React.memo(ChatMessageBubble, areEqual)