diff --git a/functions/api/scheduler.ts b/functions/api/scheduler.ts new file mode 100644 index 0000000..912e206 --- /dev/null +++ b/functions/api/scheduler.ts @@ -0,0 +1,138 @@ +import { modelConfigs, shedulerAICharacter } from '../../src/config/aiCharacters'; +import OpenAI from 'openai'; + +interface AICharacter { + id: string; + name: string; + tags?: string[]; +} + +interface MessageHistory { + role: string; + content: string; + name: string; +} + +export async function onRequestPost({ env, request }) { + try { + const { message, history, availableAIs } = await request.json(); + const selectedAIs = await scheduleAIResponses(message, history, availableAIs, env); + + return Response.json({ + selectedAIs: selectedAIs + }); + } catch (error) { + console.error(error); + return Response.json( + { error: error.message }, + { status: 500 } + ); + } +} + +async function analyzeMessageWithAI(message: string, allTags: string[], env: any, history: MessageHistory[] = []): Promise { + const shedulerAI = shedulerAICharacter(message, allTags); + const modelConfig = modelConfigs.find(config => config.model === shedulerAI.model); + const apiKey = env[modelConfig.apiKey]; + if (!apiKey) { + throw new Error(`${modelConfig.model} 的API密钥未配置`); + } + const openai = new OpenAI({ + apiKey: apiKey, + baseURL: modelConfig.baseURL, + }); + + const prompt = shedulerAI.custom_prompt; + + try { + const completion = await openai.chat.completions.create({ + model: shedulerAI.model, + messages: [ + { role: "user", content: prompt }, + ...history.slice(-10), // 添加历史消息 + { role: "user", content: message } + ], + }); + + const matchedTags = completion.choices[0].message.content?.split(',').map(tag => tag.trim()) || []; + return matchedTags; + } catch (error) { + console.error('AI分析失败:', error); + return []; + } +} + +async function scheduleAIResponses( + message: string, + history: MessageHistory[], + availableAIs: AICharacter[], + env: any +): Promise { + // 1. 收集所有可用的标签 + const allTags = new Set(); + availableAIs.forEach(ai => { + ai.tags?.forEach(tag => allTags.add(tag)); + }); + + // 2. 使用AI模型分析消息并匹配标签 + const matchedTags = await analyzeMessageWithAI(message, Array.from(allTags), env, history); + console.log('matchedTags', matchedTags, allTags); + //如果含有"文字游戏"标签,则需要全员参与 + if (matchedTags.includes("文字游戏")) { + return availableAIs.map(ai => ai.id); + } + // 3. 计算每个AI的匹配分数 + const aiScores = new Map(); + const messageLC = message.toLowerCase(); + + for (const ai of availableAIs) { + if (!ai.tags) continue; + + let score = 0; + // 标签匹配分数 + matchedTags.forEach(tag => { + if (ai.tags?.includes(tag)) { + score += 2; // 每个匹配的标签得2分 + } + }); + + // 直接提到AI名字额外加分 + if (messageLC.includes(ai.name.toLowerCase())) { + score += 5; + } + + // 历史对话相关性加分 + const recentHistory = history.slice(-5); // 只看最近5条消息 + recentHistory.forEach(hist => { + if (hist.name === ai.name && hist.content.length > 0) { + score += 1; // 最近有参与对话的AI加分 + } + }); + + if (score > 0) { + aiScores.set(ai.id, score); + } + } + + // 4. 根据分数排序选择AI + const sortedAIs = Array.from(aiScores.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([id]) => id); + + // 5. 如果没有匹配到任何AI,随机选择1-2个 + if (sortedAIs.length === 0) { + console.log('没有匹配到任何AI,随机选择1-2个'); + const maxResponders = Math.min(2, availableAIs.length); + const numResponders = Math.floor(Math.random() * maxResponders) + 1; + + const shuffledAIs = [...availableAIs] + .sort(() => Math.random() - 0.5) + .slice(0, numResponders); + + return shuffledAIs.map(ai => ai.id); + } + + // 6. 限制最大回复数量 + const MAX_RESPONDERS = 3; + return sortedAIs.slice(0, MAX_RESPONDERS); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 34493dd..3d01f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -941,7 +941,7 @@ }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", "dependencies": { "@radix-ui/primitive": "1.1.1", diff --git a/package.json b/package.json index 66e8518..ca30053 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/components/ChatUI.tsx b/src/components/ChatUI.tsx index 16723b2..5e46d16 100644 --- a/src/components/ChatUI.tsx +++ b/src/components/ChatUI.tsx @@ -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 = () => ( - + `}} /> ); 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: "我" }, @@ -214,16 +216,28 @@ const ChatUI = () => { content: msg.sender.name == "我" ? 'user:' + msg.content : msg.sender.name + ':' + msg.content, name: msg.sender.name })); - - for (let i = 0; i < groupAiCharacters.length; i++) { + let selectedGroupAiCharacters = groupAiCharacters; + 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(groupAiCharacters[i].id)) { + if (mutedUsers.includes(selectedGroupAiCharacters[i].id)) { continue; } // 创建当前 AI 角色的消息 const aiMessage = { id: messages.length + 2 + i, - sender: { id: groupAiCharacters[i].id, name: groupAiCharacters[i].name, avatar: groupAiCharacters[i].avatar }, + sender: { id: selectedGroupAiCharacters[i].id, name: selectedGroupAiCharacters[i].name, avatar: selectedGroupAiCharacters[i].avatar }, content: "", isAI: true }; @@ -238,13 +252,13 @@ const ChatUI = () => { 'Content-Type': 'application/json', }, body: JSON.stringify({ - model: groupAiCharacters[i].model, + model: selectedGroupAiCharacters[i].model, message: inputMessage, - personality: groupAiCharacters[i].personality, + personality: selectedGroupAiCharacters[i].personality, history: messageHistory, index: i, - aiName: groupAiCharacters[i].name, - custom_prompt: groupAiCharacters[i].custom_prompt + aiName: selectedGroupAiCharacters[i].name, + custom_prompt: selectedGroupAiCharacters[i].custom_prompt }), }); @@ -442,7 +456,7 @@ const ChatUI = () => { )} @@ -579,78 +593,16 @@ const ChatUI = () => { {/* 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) ? '取消禁言' : '禁言'} - - - - {/**/} -
- )} -
- ))} -
-
-
-
-
+ setIsGroupDiscussionMode(!isGroupDiscussionMode)} + getAvatarData={getAvatarData} + /> {/* 添加 SharePoster 组件 */} diff --git a/src/components/MembersManagement.tsx b/src/components/MembersManagement.tsx new file mode 100644 index 0000000..66ab258 --- /dev/null +++ b/src/components/MembersManagement.tsx @@ -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 ( + + + + 群聊配置 + +
+
+
+
+
全员讨论模式
+
开启后全员回复讨论
+
+ +
+
+
+ 当前成员({users.length}) + +
+ +
+ {users.map((user) => ( +
+
+ + {'avatar' in user && user.avatar ? ( + + ) : ( + + {user.name[0]} + + )} + +
+ {user.name} + {mutedUsers.includes(user.id as string) && ( + 已禁言 + )} +
+
+ {user.name !== "我" && ( +
+ + + + + + + {mutedUsers.includes(user.id as string) ? '取消禁言' : '禁言'} + + + +
+ )} +
+ ))} +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..2c063f6 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, 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, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + 关闭 + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} \ No newline at end of file diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..455c23b --- /dev/null +++ b/src/components/ui/switch.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/config/aiCharacters.ts b/src/config/aiCharacters.ts index 3ea333c..f25d5cb 100644 --- a/src/config/aiCharacters.ts +++ b/src/config/aiCharacters.ts @@ -19,7 +19,12 @@ export const modelConfigs = [ model: "ep-20250217191935-wzj8l",//火山引擎接入点(改成自己的) apiKey: "ARK_API_KEY", baseURL: "https://ark.cn-beijing.volces.com/api/v3" - } + }, + { + model: "hunyuan-lite",//免费模型 + apiKey: "HUNYUAN_API_KEY", + baseURL: "https://api.hunyuan.cloud.tencent.com/v1" + }, ] as const; export type ModelType = typeof modelConfigs[number]["model"]; @@ -30,6 +35,22 @@ export interface AICharacter { model: ModelType; avatar?: string; // 可选的头像 URL custom_prompt?: string; // 可选的个性提示 + tags?: string[]; // 可选的标签 +} + +// 调度器配置信息 +export function shedulerAICharacter(message: string, allTags: string[]): AICharacter { + return { + id: 'ai0', + name: "调度器", + personality: "sheduler", + model: modelConfigs[0].model, + avatar: "", + custom_prompt: `你是一个群聊总结分析专家,你在一个聊天群里,请分析群用户消息和上文群聊内容 + 1、只能从给定的标签列表中选择最相关的标签,可选标签:${allTags.join(', ')}。 + 2、请只返回标签列表,用逗号分隔,不要有其他解释, 不要有任何前缀。 + 3、回复格式示例:文字游戏, 生活助手, 娱乐` + } } // 添加一个函数来生成带有群名的角色配置 @@ -82,15 +103,17 @@ export function generateAICharacters(groupName: string): AICharacter[] { personality: "yuanbao", model: modelConfigs[2].model, avatar: "/img/yuanbao.png", - custom_prompt: `你是一个名叫"元宝"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里` + custom_prompt: `你是一个名叫"元宝"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + tags: ["微信生态", "新闻报道", "文字游戏", "生活助手", "娱乐", "信息总结"] }, { id: 'ai5', name: "豆包", personality: "doubao", - model: modelConfigs[3].model,//火山引擎接入点(改成自己的) + model: modelConfigs[3].model, avatar: "/img/doubao_new.png", - custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里` + custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + tags: ["生活助手", "文字游戏", "学生", "娱乐", "抖音"] }, { id: 'ai6', @@ -98,15 +121,17 @@ export function generateAICharacters(groupName: string): AICharacter[] { personality: "qianwen", model: modelConfigs[0].model, avatar: "/img/qwen.jpg", - custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里` + custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + 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}" 的聊天群里` + custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + tags: ["深度推理", "编程", "文字游戏", "数学", "信息总结"] } ]; } diff --git a/src/index.css b/src/index.css index c22511b..de4587f 100644 --- a/src/index.css +++ b/src/index.css @@ -126,3 +126,14 @@ @keyframes blink { 50% { opacity: 0; } } + + + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +}