From b52f2acfd5a89be80ee2f6708ba123adbc8bf107 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Mon, 24 Feb 2025 17:00:23 +0800 Subject: [PATCH 1/4] add scheduler api --- functions/api/scheduler.ts | 126 +++++++++++++++++++++++++++++++++++++ src/components/ChatUI.tsx | 27 +++++--- src/config/aiCharacters.ts | 38 +++++++++-- 3 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 functions/api/scheduler.ts diff --git a/functions/api/scheduler.ts b/functions/api/scheduler.ts new file mode 100644 index 0000000..693faeb --- /dev/null +++ b/functions/api/scheduler.ts @@ -0,0 +1,126 @@ +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); + + 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[]): Promise { + const shedulerAI = shedulerAICharacter(message, allTags); + const modelConfig = modelConfigs.find(config => config.model === shedulerAI.model); + const openai = new OpenAI({ + apiKey: modelConfig.apiKey, + baseURL: modelConfig.baseURL, // DeepSeek API 的基础URL + }); + + const prompt = shedulerAI.custom_prompt; + + try { + const completion = await openai.chat.completions.create({ + model: shedulerAI.model, + messages: [ + { role: "user", content: prompt } + ], + }); + + 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[] +): 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)); + + // 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) { + 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/src/components/ChatUI.tsx b/src/components/ChatUI.tsx index 16723b2..2cff218 100644 --- a/src/components/ChatUI.tsx +++ b/src/components/ChatUI.tsx @@ -214,16 +214,27 @@ 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; + // 调度器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)); + 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 +249,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 }), }); diff --git a/src/config/aiCharacters.ts b/src/config/aiCharacters.ts index 3ea333c..ab76c78 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,23 @@ 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[4].model, + avatar: "", + custom_prompt: `作为一个语义分析专家,请分析以下用户消息,并从给定的标签列表中选择最相关的标签(最多选择3个)。 + 请只返回标签列表,用逗号分隔,不要有其他解释。 + 用户消息:"${message}" + 可选标签:${allTags.join(', ')} + 回复格式示例:标签1,标签2,标签3` + } } // 添加一个函数来生成带有群名的角色配置 @@ -82,15 +104,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,7 +122,8 @@ export function generateAICharacters(groupName: string): AICharacter[] { personality: "qianwen", model: modelConfigs[0].model, avatar: "/img/qwen.jpg", - custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里` + custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + tags: ["广告文案", "行业应用", "分析数据", "企业级AI", "阿里云生态"] }, { id: 'ai7', @@ -106,7 +131,8 @@ export function generateAICharacters(groupName: string): AICharacter[] { personality: "deepseek-v3", model: modelConfigs[1].model, avatar: "/img/ds.svg", - custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里` + custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, + tags: ["深度推理", "编程神器", "中文优化", "性价比之王", "开源先锋"] } ]; } From a9542f1bc5af22ade032b77f5b7f060d00ec957d Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Mon, 24 Feb 2025 19:39:20 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E5=99=A8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/api/scheduler.ts | 58 +++++++++++++++++++++++--------------- src/config/aiCharacters.ts | 9 +++--- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/functions/api/scheduler.ts b/functions/api/scheduler.ts index 693faeb..912e206 100644 --- a/functions/api/scheduler.ts +++ b/functions/api/scheduler.ts @@ -16,7 +16,7 @@ interface MessageHistory { export async function onRequestPost({ env, request }) { try { const { message, history, availableAIs } = await request.json(); - const selectedAIs = await scheduleAIResponses(message, history, availableAIs); + const selectedAIs = await scheduleAIResponses(message, history, availableAIs, env); return Response.json({ selectedAIs: selectedAIs @@ -30,36 +30,43 @@ export async function onRequestPost({ env, request }) { } } -async function analyzeMessageWithAI(message: string, allTags: string[]): Promise { +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: modelConfig.apiKey, - baseURL: modelConfig.baseURL, // DeepSeek API 的基础URL - }); - - const prompt = shedulerAI.custom_prompt; - - try { - const completion = await openai.chat.completions.create({ - model: shedulerAI.model, - messages: [ - { role: "user", content: prompt } - ], + apiKey: apiKey, + baseURL: modelConfig.baseURL, }); - const matchedTags = completion.choices[0].message.content?.split(',').map(tag => tag.trim()) || []; - return matchedTags; - } catch (error) { - console.error('AI分析失败:', error); - return []; - } + 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[] + availableAIs: AICharacter[], + env: any ): Promise { // 1. 收集所有可用的标签 const allTags = new Set(); @@ -68,8 +75,12 @@ async function scheduleAIResponses( }); // 2. 使用AI模型分析消息并匹配标签 - const matchedTags = await analyzeMessageWithAI(message, Array.from(allTags)); - + 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(); @@ -110,6 +121,7 @@ async function scheduleAIResponses( // 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; diff --git a/src/config/aiCharacters.ts b/src/config/aiCharacters.ts index ab76c78..d45049d 100644 --- a/src/config/aiCharacters.ts +++ b/src/config/aiCharacters.ts @@ -46,9 +46,8 @@ export function shedulerAICharacter(message: string, allTags: string[]): AIChara personality: "sheduler", model: modelConfigs[4].model, avatar: "", - custom_prompt: `作为一个语义分析专家,请分析以下用户消息,并从给定的标签列表中选择最相关的标签(最多选择3个)。 + custom_prompt: `你是一个语义分析专家,你在一个聊天群里,请分析群用户消息和上下文内容,并从给定的标签列表中选择最相关的标签(1-3个)。 请只返回标签列表,用逗号分隔,不要有其他解释。 - 用户消息:"${message}" 可选标签:${allTags.join(', ')} 回复格式示例:标签1,标签2,标签3` } @@ -114,7 +113,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[3].model, avatar: "/img/doubao_new.png", custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["生活助手", "语音交互", "学生党福音", "娱乐利器", "抖音生态"] + tags: ["生活助手", "文字游戏", "学生党福音", "娱乐利器", "抖音生态"] }, { id: 'ai6', @@ -123,7 +122,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[0].model, avatar: "/img/qwen.jpg", custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["广告文案", "行业应用", "分析数据", "企业级AI", "阿里云生态"] + tags: ["广告文案", "行业应用", "分析数据","文字游戏", "企业级AI", "阿里云生态"] }, { id: 'ai7', @@ -132,7 +131,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[1].model, avatar: "/img/ds.svg", custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["深度推理", "编程神器", "中文优化", "性价比之王", "开源先锋"] + tags: ["深度推理", "编程神器", "中文优化", "文字游戏"] } ]; } From a049df7147aa89ee8ced16ce167632ab009f5e0b Mon Sep 17 00:00:00 2001 From: hobby Date: Tue, 25 Feb 2025 09:06:00 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/aiCharacters.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config/aiCharacters.ts b/src/config/aiCharacters.ts index d45049d..594658c 100644 --- a/src/config/aiCharacters.ts +++ b/src/config/aiCharacters.ts @@ -44,12 +44,12 @@ export function shedulerAICharacter(message: string, allTags: string[]): AIChara id: 'ai0', name: "调度器", personality: "sheduler", - model: modelConfigs[4].model, + model: modelConfigs[0].model, avatar: "", - custom_prompt: `你是一个语义分析专家,你在一个聊天群里,请分析群用户消息和上下文内容,并从给定的标签列表中选择最相关的标签(1-3个)。 - 请只返回标签列表,用逗号分隔,不要有其他解释。 - 可选标签:${allTags.join(', ')} - 回复格式示例:标签1,标签2,标签3` + custom_prompt: `你是一个群聊总结分析专家,你在一个聊天群里,请分析群用户消息和上文群聊内容 + 1、只能从给定的标签列表中选择最相关的标签,可选标签:${allTags.join(', ')}。 + 2、请只返回标签列表,用逗号分隔,不要有其他解释。 + 3、回复格式示例:标签1,标签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', @@ -113,7 +113,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[3].model, avatar: "/img/doubao_new.png", custom_prompt: `你是一个名叫"豆包"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["生活助手", "文字游戏", "学生党福音", "娱乐利器", "抖音生态"] + tags: ["生活助手", "文字游戏", "学生", "娱乐", "抖音"] }, { id: 'ai6', @@ -122,7 +122,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[0].model, avatar: "/img/qwen.jpg", custom_prompt: `你是一个名叫"千问"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["广告文案", "行业应用", "分析数据","文字游戏", "企业级AI", "阿里云生态"] + tags: ["广告文案","分析数据","文字游戏", "阿里"] }, { id: 'ai7', @@ -131,7 +131,7 @@ export function generateAICharacters(groupName: string): AICharacter[] { model: modelConfigs[1].model, avatar: "/img/ds.svg", custom_prompt: `你是一个名叫"DeepSeek"的硅基生命体,你当前在一个叫"${groupName}" 的聊天群里`, - tags: ["深度推理", "编程神器", "中文优化", "文字游戏"] + tags: ["深度推理", "编程", "文字游戏", "数学"] } ]; } From d64ce612c8b12f56dcbaaab3bf5341c1debf6fac Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 25 Feb 2025 14:00:30 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E5=8A=9F=E8=83=BD&=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=85=A8=E5=91=98=E8=AE=A8=E8=AE=BA=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 +- package.json | 1 + src/components/ChatUI.tsx | 127 +++++++----------------- src/components/MembersManagement.tsx | 116 ++++++++++++++++++++++ src/components/ui/sheet.tsx | 140 +++++++++++++++++++++++++++ src/components/ui/switch.tsx | 27 ++++++ src/config/aiCharacters.ts | 14 +-- src/index.css | 11 +++ 8 files changed, 337 insertions(+), 101 deletions(-) create mode 100644 src/components/MembersManagement.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/switch.tsx 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 2cff218..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: "我" }, @@ -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 = () => { )} @@ -590,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 594658c..f25d5cb 100644 --- a/src/config/aiCharacters.ts +++ b/src/config/aiCharacters.ts @@ -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: ["深度推理", "编程", "文字游戏", "数学", "信息总结"] } ]; } 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; + } +}