forked from 77media/video-flow
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import React, { useRef, useCallback } from "react";
|
|
import { ArrowRightFromLine, ChevronDown } from 'lucide-react';
|
|
import { Switch } from 'antd';
|
|
import { MessageRenderer } from "./MessageRenderer";
|
|
import { InputBar } from "./InputBar";
|
|
import { useMessages } from "./useMessages";
|
|
import { DateDivider } from "./DateDivider";
|
|
import { LoadMoreButton } from "./LoadMoreButton";
|
|
import { ChatMessage } from "./types";
|
|
|
|
interface SmartChatBoxProps {
|
|
isSmartChatBoxOpen: boolean;
|
|
setIsSmartChatBoxOpen: (v: boolean) => void;
|
|
projectId: string;
|
|
userId: number;
|
|
}
|
|
|
|
interface MessageGroup {
|
|
date: number;
|
|
messages: ChatMessage[];
|
|
}
|
|
|
|
function BackToLatestButton({ onClick }: { onClick: () => void }) {
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className="fixed bottom-24 right-4 bg-blue-500 hover:bg-blue-400 text-white rounded-full p-2 shadow-lg"
|
|
title="返回最新消息"
|
|
>
|
|
<ChevronDown size={20} />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default function SmartChatBox({ isSmartChatBoxOpen, setIsSmartChatBoxOpen, projectId, userId }: SmartChatBoxProps) {
|
|
// 消息列表引用
|
|
const listRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 处理消息更新时的滚动
|
|
const handleMessagesUpdate = useCallback((shouldScroll: boolean) => {
|
|
if (shouldScroll && listRef.current) {
|
|
listRef.current.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
|
|
}
|
|
}, []);
|
|
|
|
// 使用消息管理 hook
|
|
const [
|
|
{ messages, isLoading, error, hasMore, loadMoreMessages, backToLatest, isViewingHistory },
|
|
{ sendMessage },
|
|
{ enabled: systemPush, toggle: toggleSystemPush }
|
|
] = useMessages({
|
|
config: { projectId, userId },
|
|
onMessagesUpdate: handleMessagesUpdate
|
|
});
|
|
|
|
// 按日期分组消息
|
|
const groupedMessages = React.useMemo(() => {
|
|
const groups: MessageGroup[] = [];
|
|
|
|
messages.forEach(message => {
|
|
const messageDate = new Date(message.createdAt).setHours(0, 0, 0, 0);
|
|
|
|
const existingGroup = groups.find(group => {
|
|
const groupDate = new Date(group.date).setHours(0, 0, 0, 0);
|
|
return groupDate === messageDate;
|
|
});
|
|
|
|
if (existingGroup) {
|
|
existingGroup.messages.push(message);
|
|
} else {
|
|
groups.push({
|
|
date: messageDate,
|
|
messages: [message]
|
|
});
|
|
}
|
|
});
|
|
|
|
return groups.sort((a, b) => a.date - b.date);
|
|
}, [messages]);
|
|
|
|
return (
|
|
<div className="h-full w-full text-gray-100 flex flex-col" data-alt="smart-chat-box">
|
|
{/* Header */}
|
|
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between" data-alt="chat-header">
|
|
<div className="font-semibold flex items-center gap-2">
|
|
<span>Chat</span>
|
|
{/* System push toggle */}
|
|
<Switch
|
|
checkedChildren="系统推送:开"
|
|
unCheckedChildren="系统推送:关"
|
|
checked={systemPush}
|
|
onChange={toggleSystemPush}
|
|
className="ml-2"
|
|
/>
|
|
</div>
|
|
<div className="text-xs opacity-70">
|
|
<ArrowRightFromLine
|
|
className="w-4 h-4 cursor-pointer"
|
|
onClick={() => setIsSmartChatBoxOpen(false)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Message list */}
|
|
<div ref={listRef} className="flex-1 overflow-y-auto p-4" data-alt="message-list">
|
|
{/* Load more button */}
|
|
{hasMore && (
|
|
<LoadMoreButton onClick={loadMoreMessages} loading={isLoading} />
|
|
)}
|
|
|
|
{/* Messages grouped by date */}
|
|
<div className="space-y-3">
|
|
{groupedMessages.map((group) => (
|
|
<React.Fragment key={group.date}>
|
|
<DateDivider timestamp={group.date} />
|
|
{group.messages.map((message) => (
|
|
<MessageRenderer key={message.id} msg={message} />
|
|
))}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
|
|
{/* Loading indicator */}
|
|
{isLoading && !hasMore && (
|
|
<div className="flex justify-center py-2">
|
|
<div className="animate-spin rounded-full h-5 w-5 border-2 border-gray-400 border-t-white" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Error message */}
|
|
{error && (
|
|
<div className="text-red-500 text-center py-2 text-sm">
|
|
{error.message}
|
|
</div>
|
|
)}
|
|
|
|
{/* Back to latest button */}
|
|
{isViewingHistory && (
|
|
<BackToLatestButton onClick={backToLatest} />
|
|
)}
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<InputBar onSend={sendMessage} />
|
|
</div>
|
|
);
|
|
} |