import React, { useState, useRef, useEffect } from 'react'; import { Send, Menu, MoreHorizontal, UserPlus, UserMinus, Users2, Users, MoreVertical, Share2, Mic, MicOff } from 'lucide-react'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import {generateAICharacters} from "@/config/aiCharacters"; import { groups } from "@/config/groups"; import type { AICharacter } from "@/config/aiCharacters"; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import html2canvas from 'html2canvas'; import { SharePoster } from '@/components/SharePoster'; // 使用本地头像数据,避免外部依赖 const getAvatarData = (name: string) => { const colors = ['#1abc9c', '#3498db', '#9b59b6', '#f1c40f', '#e67e22']; const index = (name.charCodeAt(0) + (name.charCodeAt(1) || 0 )) % colors.length; return { backgroundColor: colors[index], text: name[0], }; }; // 单个完整头像 const SingleAvatar = ({ user }: { user: User | AICharacter }) => { // 如果有头像就使用头像,否则使用默认的文字头像 if ('avatar' in user && user.avatar) { return (
{user.name}
); } const avatarData = getAvatarData(user.name); return (
{avatarData.text}
); }; // 左右分半头像 const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => { if ('avatar' in user && user.avatar) { return (
{user.name}
); } const avatarData = getAvatarData(user.name); return (
{avatarData.text}
); }; // 四分之一头像 const QuarterAvatar = ({ user, index }: { user: User, index: number }) => { if ('avatar' in user && user.avatar) { return (
{user.name}
); } const avatarData = getAvatarData(user.name); return (
{avatarData.text}
); }; // 动态导入 KaTeX 样式 const KaTeXStyle = () => ( ); const ChatUI = () => { const [group, setGroup] = useState(groups[1]); const groupAiCharacters = generateAICharacters(group.name).filter(character => group.members.includes(character.id)); const [users, setUsers] = useState([ { id: 1, name: "我" }, ...groupAiCharacters ]); const [showMembers, setShowMembers] = useState(false); const [messages, setMessages] = useState([ ]); const [inputMessage, setInputMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [pendingContent, setPendingContent] = useState(""); const [isTyping, setIsTyping] = useState(false); const currentMessageRef = useRef(null); const typewriterRef = useRef(null); const accumulatedContentRef = useRef(""); // 用于跟踪完整内容 const messagesEndRef = useRef(null); // 添加禁言状态 const [mutedUsers, setMutedUsers] = useState([]); const abortController = new AbortController(); const handleRemoveUser = (userId: number) => { setUsers(users.filter(user => user.id !== userId)); }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); // 添加禁言/取消禁言处理函数 const handleToggleMute = (userId: string) => { setMutedUsers(prev => prev.includes(userId) ? prev.filter(id => id !== userId) : [...prev, userId] ); }; const handleSendMessage = async () => { if (!inputMessage.trim()) return; // 添加用户消息 const userMessage = { id: messages.length + 1, sender: users[0], content: inputMessage, isAI: false }; setMessages(prev => [...prev, userMessage]); setInputMessage(""); setIsLoading(true); setPendingContent(""); accumulatedContentRef.current = ""; // 构建历史消息数组 let messageHistory = messages.map(msg => ({ role: 'user', content: msg.sender.name == "我" ? 'user:' + msg.content : msg.sender.name + ':' + msg.content, name: msg.sender.name })); for (let i = 0; i < groupAiCharacters.length; i++) { //禁言 if (mutedUsers.includes(groupAiCharacters[i].id)) { continue; } // 创建当前 AI 角色的消息 const aiMessage = { id: messages.length + 2 + i, sender: { id: groupAiCharacters[i].id, name: groupAiCharacters[i].name, avatar: groupAiCharacters[i].avatar }, content: "", isAI: true }; // 添加当前 AI 的消息 setMessages(prev => [...prev, aiMessage]); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: groupAiCharacters[i].model, message: inputMessage, personality: groupAiCharacters[i].personality, history: messageHistory, index: i, aiName: groupAiCharacters[i].name, custom_prompt: groupAiCharacters[i].custom_prompt }), }); if (!response.ok) { throw new Error('请求失败'); } const reader = response.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('无法获取响应流'); } let buffer = ''; let completeResponse = ''; // 用于跟踪完整的响应 while (true) { const { done, value } = await reader.read(); if (done) { //如果completeResponse为空, if (completeResponse.trim() === "") { completeResponse = "这个问题难倒我了,下一位。"; setMessages(prev => { const newMessages = [...prev]; const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id); if (aiMessageIndex !== -1) { newMessages[aiMessageIndex] = { ...newMessages[aiMessageIndex], content: completeResponse }; } return newMessages; });} break; } buffer += decoder.decode(value, { stream: true }); let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); if (data.content) { completeResponse += data.content; setMessages(prev => { const newMessages = [...prev]; const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id); if (aiMessageIndex !== -1) { newMessages[aiMessageIndex] = { ...newMessages[aiMessageIndex], content: completeResponse }; } return newMessages; }); } } catch (e) { console.error('解析响应数据失败:', e); } } } } // 将当前AI的回复添加到消息历史中,供下一个AI使用 messageHistory.push({ role: 'user', content: aiMessage.sender.name + ':' + completeResponse, name: aiMessage.sender.name }); // 等待一小段时间再开始下一个 AI 的回复 if (i < groupAiCharacters.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } catch (error) { console.error("发送消息失败:", error); setMessages(prev => prev.map(msg => msg.id === aiMessage.id ? { ...msg, content: "错误: " + error.message, isError: true } : msg )); } } setIsLoading(false); }; const handleCancel = () => { abortController.abort(); }; // 清理打字机效果 useEffect(() => { return () => { if (typewriterRef.current) { clearInterval(typewriterRef.current); } }; }, []); // 添加对聊天区域的引用 const chatAreaRef = useRef(null); // 更新分享函数 const [showPoster, setShowPoster] = useState(false); const handleShareChat = () => { setShowPoster(true); }; return ( <>
{/* Header */}
{/* 左侧群组信息 */}
{users.length === 1 ? ( ) : users.length === 2 ? (
{users.slice(0, 2).map((user, index) => ( ))}
) : users.length === 3 ? (
{users.slice(0, 2).map((user, index) => ( ))}
) : (
{users.slice(0, 4).map((user, index) => ( ))}
)}

{group.name}

{users.length} 名成员

{/* 右侧头像组和按钮 */}
{users.slice(0, 4).map((user) => { const avatarData = getAvatarData(user.name); return ( {'avatar' in user && user.avatar ? ( ) : ( {avatarData.text} )}

{user.name}

); })} {users.length > 4 && (
+{users.length - 4}
)}
{/* Main Chat Area */}
{messages.map((message) => (
{message.sender.name !== "我" && ( {'avatar' in message.sender && message.sender.avatar ? ( ) : ( {message.sender.name[0]} )} )}
{message.sender.name}
{message.content} {message.isAI && isTyping && currentMessageRef.current === message.id && ( )}
{message.sender.name === "我" && ( {'avatar' in message.sender && message.sender.avatar ? ( ) : ( {message.sender.name[0]} )} )}
))}
{/* Input Area */}

分享聊天记录

setInputMessage(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} />
{/* Members Management Dialog */} 群成员管理
当前成员({users.length})
{users.map((user) => (
{'avatar' in user && user.avatar ? ( ) : ( {user.name[0]} )}
{user.name} {mutedUsers.includes(user.id) && ( 已禁言 )}
{user.name !== "我" && (
{mutedUsers.includes(user.id) ? '取消禁言' : '禁言'} {/**/}
)}
))}
{/* 添加 SharePoster 组件 */} setShowPoster(false)} chatAreaRef={chatAreaRef} /> ); }; export default ChatUI;