新增模型调度功能&增加全员讨论开关

This commit is contained in:
maojindao55
2025-02-25 14:00:30 +08:00
parent a049df7147
commit d64ce612c8
8 changed files with 337 additions and 101 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { Send, Menu, MoreHorizontal, UserPlus, UserMinus, Users2, Users, MoreVertical, Share2, Mic, MicOff } from 'lucide-react';
import { Send, Menu, MoreHorizontal, UserPlus, UserMinus, Users2, Users, MoreVertical, Share2, Mic, MicOff, Settings2 } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -12,11 +12,11 @@ import {
} from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import {generateAICharacters} from "@/config/aiCharacters";
import { groups } from "@/config/groups";
@@ -27,6 +27,7 @@ import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import html2canvas from 'html2canvas';
import { SharePoster } from '@/components/SharePoster';
import { MembersManagement } from '@/components/MembersManagement';
// 使用本地头像数据,避免外部依赖
const getAvatarData = (name: string) => {
@@ -117,9 +118,9 @@ const QuarterAvatar = ({ user, index }: { user: User, index: number }) => {
);
};
// 动态导入 KaTeX 样式
// 修改 KaTeXStyle 组件
const KaTeXStyle = () => (
<style jsx global>{`
<style dangerouslySetInnerHTML={{ __html: `
/* 只在聊天消息内应用 KaTeX 样式 */
.chat-message .katex-html {
display: none;
@@ -141,11 +142,12 @@ const KaTeXStyle = () => (
/* 其他必要的 KaTeX 样式 */
@import "katex/dist/katex.min.css";
`}</style>
`}} />
);
const ChatUI = () => {
const [group, setGroup] = useState(groups[1]);
const [isGroupDiscussionMode, setIsGroupDiscussionMode] = useState(false);
const groupAiCharacters = generateAICharacters(group.name).filter(character => group.members.includes(character.id));
const [users, setUsers] = useState([
{ id: 1, name: "我" },
@@ -215,17 +217,18 @@ const ChatUI = () => {
name: msg.sender.name
}));
let selectedGroupAiCharacters = groupAiCharacters;
// 调度器api请求
const shedulerResponse = await fetch('/api/scheduler', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: inputMessage, history: messageHistory, availableAIs: groupAiCharacters })
});
const shedulerData = await shedulerResponse.json();
const selectedAIs = shedulerData.selectedAIs;
selectedGroupAiCharacters = selectedAIs.map(ai => groupAiCharacters.find(c => c.id === ai));
if (!isGroupDiscussionMode) {
const shedulerResponse = await fetch('/api/scheduler', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: inputMessage, history: messageHistory, availableAIs: groupAiCharacters })
});
const shedulerData = await shedulerResponse.json();
const selectedAIs = shedulerData.selectedAIs;
selectedGroupAiCharacters = selectedAIs.map(ai => groupAiCharacters.find(c => c.id === ai));
}
for (let i = 0; i < selectedGroupAiCharacters.length; i++) {
//禁言
if (mutedUsers.includes(selectedGroupAiCharacters[i].id)) {
@@ -453,7 +456,7 @@ const ChatUI = () => {
)}
</div>
<Button variant="ghost" size="icon" onClick={() => setShowMembers(true)}>
<Users className="w-5 h-5" />
<Settings2 className="w-5 h-5" />
</Button>
</div>
</div>
@@ -590,78 +593,16 @@ const ChatUI = () => {
</div>
{/* Members Management Dialog */}
<Dialog open={showMembers} onOpenChange={setShowMembers}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="mt-4">
<div className="flex justify-between items-center mb-4">
<span className="text-sm text-gray-500">{users.length}</span>
<Button variant="outline" size="sm">
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</div>
<ScrollArea className="h-[300px]">
<div className="space-y-2">
{users.map((user) => (
<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">
<Avatar>
{'avatar' in user && user.avatar ? (
<AvatarImage src={user.avatar} className="w-10 h-10" />
) : (
<AvatarFallback style={{ backgroundColor: getAvatarData(user.name).backgroundColor, color: 'white' }}>
{user.name[0]}
</AvatarFallback>
)}
</Avatar>
<div className="flex flex-col">
<span>{user.name}</span>
{mutedUsers.includes(user.id) && (
<span className="text-xs text-red-500"></span>
)}
</div>
</div>
{user.name !== "我" && (
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
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>
</ScrollArea>
</div>
</DialogContent>
</Dialog>
<MembersManagement
showMembers={showMembers}
setShowMembers={setShowMembers}
users={users}
mutedUsers={mutedUsers}
handleToggleMute={handleToggleMute}
isGroupDiscussionMode={isGroupDiscussionMode}
onToggleGroupDiscussion={() => setIsGroupDiscussionMode(!isGroupDiscussionMode)}
getAvatarData={getAvatarData}
/>
</div>
{/* 添加 SharePoster 组件 */}

View File

@@ -0,0 +1,116 @@
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { UserPlus, Mic, MicOff } from 'lucide-react';
import { type AICharacter } from "@/config/aiCharacters";
import { Switch } from "@/components/ui/switch";
interface User {
id: number | string;
name: string;
avatar?: string;
}
interface MembersManagementProps {
showMembers: boolean;
setShowMembers: (show: boolean) => void;
users: (User | AICharacter)[];
mutedUsers: string[];
handleToggleMute: (userId: string) => void;
getAvatarData: (name: string) => { backgroundColor: string; text: string };
isGroupDiscussionMode: boolean;
onToggleGroupDiscussion: () => void;
}
export const MembersManagement = ({
showMembers,
setShowMembers,
users,
mutedUsers,
handleToggleMute,
getAvatarData,
isGroupDiscussionMode,
onToggleGroupDiscussion
}: MembersManagementProps) => {
return (
<Sheet open={showMembers} onOpenChange={setShowMembers}>
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
<SheetHeader>
<SheetTitle></SheetTitle>
</SheetHeader>
<div className="mt-4">
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<div className="text-sm"></div>
<div className="text-xs text-gray-500"></div>
</div>
<Switch
checked={isGroupDiscussionMode}
onCheckedChange={onToggleGroupDiscussion}
/>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<span className="text-sm text-gray-500">{users.length}</span>
<Button variant="outline" size="sm">
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</div>
<ScrollArea className="h-[calc(100vh-150px)]">
<div className="space-y-2 pr-4">
{users.map((user) => (
<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">
<Avatar>
{'avatar' in user && user.avatar ? (
<AvatarImage src={user.avatar} className="w-10 h-10" />
) : (
<AvatarFallback style={{ backgroundColor: getAvatarData(user.name).backgroundColor, color: 'white' }}>
{user.name[0]}
</AvatarFallback>
)}
</Avatar>
<div className="flex flex-col">
<span>{user.name}</span>
{mutedUsers.includes(user.id as string) && (
<span className="text-xs text-red-500"></span>
)}
</div>
</div>
{user.name !== "我" && (
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => handleToggleMute(user.id as string)}
>
{mutedUsers.includes(user.id as string) ? (
<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 as string) ? '取消禁言' : '禁言'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
))}
</div>
</ScrollArea>
</div>
</SheetContent>
</Sheet>
);
};

140
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only"></span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -48,8 +48,8 @@ export function shedulerAICharacter(message: string, allTags: string[]): AIChara
avatar: "",
custom_prompt: `你是一个群聊总结分析专家,你在一个聊天群里,请分析群用户消息和上文群聊内容
1、只能从给定的标签列表中选择最相关的标签可选标签${allTags.join(', ')}
2、请只返回标签列表用逗号分隔不要有其他解释。
3、回复格式示例标签1,标签2,标签3`
2、请只返回标签列表用逗号分隔不要有其他解释, 不要有任何前缀
3、回复格式示例文字游戏, 生活助手, 娱乐`
}
}
@@ -104,7 +104,7 @@ export function generateAICharacters(groupName: string): AICharacter[] {
model: modelConfigs[2].model,
avatar: "/img/yuanbao.png",
custom_prompt: `你是一个名叫"元宝"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`,
tags: ["微信生态", "新闻搜索", "文字游戏", "生活助手", "娱乐", "信息总结"]
tags: ["微信生态", "新闻报道", "文字游戏", "生活助手", "娱乐", "信息总结"]
},
{
id: 'ai5',
@@ -122,16 +122,16 @@ export function generateAICharacters(groupName: string): AICharacter[] {
model: modelConfigs[0].model,
avatar: "/img/qwen.jpg",
custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`,
tags: ["广告文案","分析数据","文字游戏", "阿里"]
tags: ["广告文案","分析数据","文字游戏","信息总结", "阿里"]
},
{
id: 'ai7',
name: "DeepSeek",
personality: "deepseek-v3",
model: modelConfigs[1].model,
personality: "deepseek-r1",
model: modelConfigs[3].model,
avatar: "/img/ds.svg",
custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`,
tags: ["深度推理", "编程", "文字游戏", "数学"]
tags: ["深度推理", "编程", "文字游戏", "数学", "信息总结"]
}
];
}

View File

@@ -126,3 +126,14 @@
@keyframes blink {
50% { opacity: 0; }
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}