优化头像和增加禁言功能
This commit is contained in:
BIN
assets/doubao.png
Normal file
BIN
assets/doubao.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/doubao_new.png
Normal file
BIN
assets/doubao_new.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
BIN
chat_bubble.ico
Normal file
BIN
chat_bubble.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/x-icon" href="/chat_bubble.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<title>AI机器人群聊</title>
|
<title>AI机器人群聊</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Send, Menu, MoreHorizontal, UserPlus, UserMinus, Users2, Users, MoreVertical, MessageCircle } from 'lucide-react';
|
import { Send, Menu, MoreHorizontal, UserPlus, UserMinus, Users2, Users, MoreVertical, Mic, MicOff } from 'lucide-react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
@@ -56,6 +56,18 @@ const SingleAvatar = ({ user }: { user: User | AICharacter }) => {
|
|||||||
|
|
||||||
// 左右分半头像
|
// 左右分半头像
|
||||||
const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => {
|
const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => {
|
||||||
|
if ('avatar' in user && user.avatar) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-1/2 h-full"
|
||||||
|
style={{
|
||||||
|
borderRight: isFirst ? '1px solid white' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
const avatarData = getAvatarData(user.name);
|
const avatarData = getAvatarData(user.name);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -72,6 +84,19 @@ const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => {
|
|||||||
|
|
||||||
// 四分之一头像
|
// 四分之一头像
|
||||||
const QuarterAvatar = ({ user, index }: { user: User, index: number }) => {
|
const QuarterAvatar = ({ user, index }: { user: User, index: number }) => {
|
||||||
|
if ('avatar' in user && user.avatar) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="aspect-square"
|
||||||
|
style={{
|
||||||
|
borderRight: index % 2 === 0 ? '1px solid white' : 'none',
|
||||||
|
borderBottom: index < 2 ? '1px solid white' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
const avatarData = getAvatarData(user.name);
|
const avatarData = getAvatarData(user.name);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -107,6 +132,9 @@ const ChatUI = () => {
|
|||||||
const accumulatedContentRef = useRef(""); // 用于跟踪完整内容
|
const accumulatedContentRef = useRef(""); // 用于跟踪完整内容
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 添加禁言状态
|
||||||
|
const [mutedUsers, setMutedUsers] = useState<string[]>([]);
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
const handleRemoveUser = (userId: number) => {
|
const handleRemoveUser = (userId: number) => {
|
||||||
@@ -121,6 +149,15 @@ const ChatUI = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// 添加禁言/取消禁言处理函数
|
||||||
|
const handleToggleMute = (userId: string) => {
|
||||||
|
setMutedUsers(prev =>
|
||||||
|
prev.includes(userId)
|
||||||
|
? prev.filter(id => id !== userId)
|
||||||
|
: [...prev, userId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!inputMessage.trim()) return;
|
if (!inputMessage.trim()) return;
|
||||||
|
|
||||||
@@ -144,12 +181,15 @@ const ChatUI = () => {
|
|||||||
name: msg.sender.name
|
name: msg.sender.name
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 依次请求两个 AI 的回复
|
|
||||||
for (let i = 0; i < groupAiCharacters.length; i++) {
|
for (let i = 0; i < groupAiCharacters.length; i++) {
|
||||||
|
//禁言
|
||||||
|
if (mutedUsers.includes(groupAiCharacters[i].id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// 创建当前 AI 角色的消息
|
// 创建当前 AI 角色的消息
|
||||||
const aiMessage = {
|
const aiMessage = {
|
||||||
id: messages.length + 2 + i,
|
id: messages.length + 2 + i,
|
||||||
sender: { id: groupAiCharacters[i].id, name: groupAiCharacters[i].name },
|
sender: { id: groupAiCharacters[i].id, name: groupAiCharacters[i].name, avatar: groupAiCharacters[i].avatar },
|
||||||
content: "",
|
content: "",
|
||||||
isAI: true
|
isAI: true
|
||||||
};
|
};
|
||||||
@@ -287,19 +327,16 @@ const ChatUI = () => {
|
|||||||
{/* 左侧群组信息 */}
|
{/* 左侧群组信息 */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="relative w-10 h-10">
|
<div className="relative w-10 h-10">
|
||||||
<div className="w-full h-full rounded-full border-2 border-white overflow-hidden bg-white">
|
<div className="w-full h-full overflow-hidden bg-white border border-gray-200">
|
||||||
{users.length === 1 ? (
|
{users.length === 1 ? (
|
||||||
// 单个成员时显示一个大头像
|
|
||||||
<SingleAvatar user={users[0]} />
|
<SingleAvatar user={users[0]} />
|
||||||
) : users.length === 2 ? (
|
) : users.length === 2 ? (
|
||||||
// 两个成员时左右分布
|
|
||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{users.slice(0, 2).map((user, index) => (
|
{users.slice(0, 2).map((user, index) => (
|
||||||
<HalfAvatar key={user.id} user={user} isFirst={index === 0} />
|
<HalfAvatar key={user.id} user={user} isFirst={index === 0} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : users.length === 3 ? (
|
) : users.length === 3 ? (
|
||||||
// 三个成员时上2下1
|
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex h-1/2">
|
<div className="flex h-1/2">
|
||||||
{users.slice(0, 2).map((user, index) => (
|
{users.slice(0, 2).map((user, index) => (
|
||||||
@@ -311,7 +348,6 @@ const ChatUI = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 四个或更多成员时显示2x2网格
|
|
||||||
<div className="h-full grid grid-cols-2">
|
<div className="h-full grid grid-cols-2">
|
||||||
{users.slice(0, 4).map((user, index) => (
|
{users.slice(0, 4).map((user, index) => (
|
||||||
<QuarterAvatar key={user.id} user={user} index={index} />
|
<QuarterAvatar key={user.id} user={user} index={index} />
|
||||||
@@ -319,7 +355,7 @@ const ChatUI = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-0.5 -right-0.5 bg-green-500 w-3 h-3 rounded-full border-2 border-white"></div>
|
<div className="absolute -bottom-0.5 -right-0.5 bg-green-500 w-3 h-3 border-2 border-white"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-medium text-base">{group.name}</h1>
|
<h1 className="font-medium text-base">{group.name}</h1>
|
||||||
@@ -337,9 +373,13 @@ const ChatUI = () => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Avatar className="w-7 h-7 border-2 border-white">
|
<Avatar className="w-7 h-7 border-2 border-white">
|
||||||
<AvatarFallback style={{ backgroundColor: avatarData.backgroundColor, color: 'white' }}>
|
{'avatar' in user && user.avatar ? (
|
||||||
{avatarData.text}
|
<AvatarImage src={user.avatar} />
|
||||||
</AvatarFallback>
|
) : (
|
||||||
|
<AvatarFallback style={{ backgroundColor: avatarData.backgroundColor, color: 'white' }}>
|
||||||
|
{avatarData.text}
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -371,9 +411,13 @@ const ChatUI = () => {
|
|||||||
className={`flex items-start gap-2 ${message.sender.name === "我" ? "justify-end" : ""}`}>
|
className={`flex items-start gap-2 ${message.sender.name === "我" ? "justify-end" : ""}`}>
|
||||||
{message.sender.name !== "我" && (
|
{message.sender.name !== "我" && (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
|
{'avatar' in message.sender && message.sender.avatar ? (
|
||||||
|
<AvatarImage src={message.sender.avatar} className="w-10 h-10" />
|
||||||
|
) : (
|
||||||
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
|
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
|
||||||
{message.sender.name[0]}
|
{message.sender.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
<div className={message.sender.name === "我" ? "text-right" : ""}>
|
<div className={message.sender.name === "我" ? "text-right" : ""}>
|
||||||
@@ -385,6 +429,8 @@ const ChatUI = () => {
|
|||||||
className={`prose dark:prose-invert max-w-none ${
|
className={`prose dark:prose-invert max-w-none ${
|
||||||
message.sender.name === "我" ? "text-white [&_*]:text-white" : ""
|
message.sender.name === "我" ? "text-white [&_*]:text-white" : ""
|
||||||
}
|
}
|
||||||
|
[&_h3]:py-1.5
|
||||||
|
[&_h3]:m-0
|
||||||
[&_p]:m-0
|
[&_p]:m-0
|
||||||
[&_pre]:bg-gray-900
|
[&_pre]:bg-gray-900
|
||||||
[&_pre]:p-2
|
[&_pre]:p-2
|
||||||
@@ -419,9 +465,13 @@ const ChatUI = () => {
|
|||||||
</div>
|
</div>
|
||||||
{message.sender.name === "我" && (
|
{message.sender.name === "我" && (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
|
{'avatar' in message.sender && message.sender.avatar ? (
|
||||||
|
<AvatarImage src={message.sender.avatar} className="w-10 h-10" />
|
||||||
|
) : (
|
||||||
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
|
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
|
||||||
{message.sender.name[0]}
|
{message.sender.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -475,21 +525,52 @@ const ChatUI = () => {
|
|||||||
<div key={user.id} className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg">
|
<div key={user.id} className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarFallback style={{ backgroundColor: getAvatarData(user.name).backgroundColor, color: 'white' }}>
|
{'avatar' in user && user.avatar ? (
|
||||||
{user.name[0]}
|
<AvatarImage src={user.avatar} className="w-10 h-10" />
|
||||||
</AvatarFallback>
|
) : (
|
||||||
|
<AvatarFallback style={{ backgroundColor: getAvatarData(user.name).backgroundColor, color: 'white' }}>
|
||||||
|
{user.name[0]}
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{user.name}</span>
|
<div className="flex flex-col">
|
||||||
|
<span>{user.name}</span>
|
||||||
|
{mutedUsers.includes(user.id) && (
|
||||||
|
<span className="text-xs text-red-500">已禁言</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*user.name !== "我" && (
|
{user.name !== "我" && (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="ghost"
|
<TooltipProvider>
|
||||||
size="icon"
|
<Tooltip>
|
||||||
onClick={() => handleRemoveUser(user.id)}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<UserMinus className="w-4 h-4 text-red-500" />
|
variant="ghost"
|
||||||
</Button>
|
size="icon"
|
||||||
)*/}
|
onClick={() => handleToggleMute(user.id)}
|
||||||
|
>
|
||||||
|
{mutedUsers.includes(user.id) ? (
|
||||||
|
<MicOff className="w-4 h-4 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<Mic className="w-4 h-4 text-green-500" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{mutedUsers.includes(user.id) ? '取消禁言' : '禁言'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
{/*<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleRemoveUser(user.id)}
|
||||||
|
>
|
||||||
|
<UserMinus className="w-4 h-4 text-red-500" />
|
||||||
|
</Button>*/}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function generateAICharacters(groupName: string): AICharacter[] {
|
|||||||
name: "豆包",
|
name: "豆包",
|
||||||
personality: "doubao",
|
personality: "doubao",
|
||||||
model: "ep-20250217191935-wzj8l",
|
model: "ep-20250217191935-wzj8l",
|
||||||
avatar: "https://lf-flow-web-cdn.doubao.com/obj/flow-doubao/samantha/logo-icon-white-bg.png",
|
avatar: "/assets/doubao_new.png",
|
||||||
custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`
|
custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user