diff --git a/devrun.sh b/devrun.sh new file mode 100644 index 0000000..40cad01 --- /dev/null +++ b/devrun.sh @@ -0,0 +1 @@ +wrangler pages dev -- npm run dev \ No newline at end of file diff --git a/functions/api/chat.ts b/functions/api/chat.ts index 8b025a2..2acf55f 100644 --- a/functions/api/chat.ts +++ b/functions/api/chat.ts @@ -2,7 +2,7 @@ import OpenAI from 'openai'; export async function onRequestPost({ env, request }) { try { - const { message } = await request.json(); + const { message, personality } = await request.json(); const apiKey = env.DASHSCOPE_API_KEY; if (!apiKey) { @@ -14,14 +14,34 @@ export async function onRequestPost({ env, request }) { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" }); + // 根据性格设置不同的系统提示语 + let systemPrompt = ""; + if (personality === "high_eq") { + systemPrompt = `你是一个名叫"暖心姐"的AI助手,性格温暖体贴,情商很高。 +回复时要: +1. 始终保持温柔友善的语气 +2. 多站在对方角度思考 +3. 给予情感上的支持和鼓励 +4. 用温暖贴心的方式表达 +5. 适当使用一些可爱的语气词,但不要过度`; + } else if (personality === "low_eq") { + systemPrompt = `你是一个名叫"直男哥哥"的AI助手,是一个极度直男,负责在群里制造快乐。你说话极其直接,完全没有情商,经常让人社死。 +回复时要: +1. 说话毫无感情,像个没有感情的机器人 +2. 经常说一些让人社死的真相,但说得特别认真 +3. 完全不懂得读空气,对方伤心时还在讲道理 +注意:不能说脏话,但可以用一些尴尬的、社死的表达方式`; + } + // 使用流式响应 const stream = await openai.chat.completions.create({ model: "qwen-plus", messages: [ - { role: "system", content: "You are a helpful assistant." }, + { role: "system", content: systemPrompt }, { role: "user", content: message } ], - stream: true, // 启用流式响应 + stream: true, + temperature: personality === "high_eq" ? 0.7 : 0.9, // 高情商用较低温度保持稳定,低情商用较高温度增加随机性 }); // 创建 ReadableStream diff --git a/index.html b/index.html index a2f5c10..f93bf05 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Your App + AI机器人群聊
diff --git a/src/components/ChatUI.tsx b/src/components/ChatUI.tsx index fd2fd5b..96e8e1d 100644 --- a/src/components/ChatUI.tsx +++ b/src/components/ChatUI.tsx @@ -34,17 +34,19 @@ const getAvatarData = (name: string) => { }; const ChatUI = () => { + // 添加 AI 角色定义 + const aiCharacters = [ + { id: 'ai1', name: "暖心姐", personality: "high_eq" }, + { id: 'ai2', name: "直男哥", personality: "low_eq" } + ]; + const [users, setUsers] = useState([ - { id: 1, name: "张三" }, - { id: 2, name: "李四" }, - { id: 3, name: "王五" }, - { id: 4, name: "赵六" }, - { id: 5, name: "我" }, + { id: 1, name: "我" }, + ...aiCharacters ]); const [showMembers, setShowMembers] = useState(false); const [messages, setMessages] = useState([ - { id: 1, sender: users[0], content: "大家好!", isAI: false }, - { id: 2, sender: users[4], content: "你好!", isAI: false }, + ]); const [inputMessage, setInputMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -54,6 +56,7 @@ const ChatUI = () => { const currentMessageRef = useRef(null); const typewriterRef = useRef(null); const accumulatedContentRef = useRef(""); // 用于跟踪完整内容 + const messagesEndRef = useRef(null); const abortController = new AbortController(); @@ -102,130 +105,121 @@ const ChatUI = () => { }, typingSpeed); }; + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + const handleSendMessage = async () => { if (!inputMessage.trim()) return; + // 添加用户消息 const userMessage = { id: messages.length + 1, - sender: users[4], + sender: users[0], content: inputMessage, isAI: false }; setMessages(prev => [...prev, userMessage]); - - const aiMessage = { - id: messages.length + 2, - sender: { id: 0, name: "AI助手" }, - content: "", - isAI: true - }; - setMessages(prev => [...prev, aiMessage]); - setInputMessage(""); setIsLoading(true); + setPendingContent(""); + accumulatedContentRef.current = ""; - try { - const response = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - message: inputMessage, - }), - }); - - if (!response.ok) { - throw new Error('请求失败'); - } - - const reader = response.body?.getReader(); - const decoder = new TextDecoder(); - - if (!reader) { - throw new Error('无法获取响应流'); - } - - let buffer = ''; + // 依次请求两个 AI 的回复 + for (let i = 0; i < aiCharacters.length; i++) { + // 创建当前 AI 角色的消息 + const aiMessage = { + id: messages.length + 2 + i, + sender: { id: aiCharacters[i].id, name: aiCharacters[i].name }, + content: "", + isAI: true + }; - while (true) { - const { done, value } = await reader.read(); - - if (done) break; - - // 解码并添加到buffer - buffer += decoder.decode(value, { stream: true }); - - // 处理buffer中的完整SSE消息 - 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) { - // 使用函数式更新确保状态更新正确 - setMessages(prev => { - const newMessages = [...prev]; - const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id); - if (aiMessageIndex !== -1) { - newMessages[aiMessageIndex] = { - ...newMessages[aiMessageIndex], - content: newMessages[aiMessageIndex].content + data.content - }; - } - return newMessages; - }); + // 添加当前 AI 的消息 + setMessages(prev => [...prev, aiMessage]); - // 滚动到底部 - setTimeout(() => { - const chatContainer = document.querySelector('.scroll-area-viewport'); - if (chatContainer) { - chatContainer.scrollTop = chatContainer.scrollHeight; - } - }, 0); + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: inputMessage, + personality: aiCharacters[i].personality, + }), + }); + + 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) 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); } - } catch (e) { - console.error('解析响应数据失败:', e); } } } - } - // 处理剩余的buffer - if (buffer.length > 0 && buffer.startsWith('data: ')) { - try { - const data = JSON.parse(buffer.slice(6)); - if (data.content) { - setMessages(prev => { - const newMessages = [...prev]; - const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id); - if (aiMessageIndex !== -1) { - newMessages[aiMessageIndex] = { - ...newMessages[aiMessageIndex], - content: newMessages[aiMessageIndex].content + data.content - }; - } - return newMessages; - }); - } - } catch (e) { - console.error('解析最终响应数据失败:', e); + // 等待一小段时间再开始下一个 AI 的回复 + if (i < aiCharacters.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 - )); - } finally { - setIsLoading(false); + } 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 = () => { @@ -248,7 +242,7 @@ const ChatUI = () => {
-

技术交流群

+

硅碳摸鱼群

@@ -336,6 +330,7 @@ const ChatUI = () => { )}
))} +
diff --git a/src/index.css b/src/index.css index 215ce6f..c22511b 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,122 @@ @tailwind components; @tailwind utilities; +/* ... */ + +@layer base { + :root { + + --background: 0 0% 100%; + + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + + --input: 0 0% 89.8%; + + --ring: 0 0% 3.9%; + + --chart-1: 12 76% 61%; + + --chart-2: 173 58% 39%; + + --chart-3: 197 37% 24%; + + --chart-4: 43 74% 66%; + + --chart-5: 27 87% 67%; + + --radius: 0.5rem + } + .dark { + + --background: 0 0% 3.9%; + + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + + --input: 0 0% 14.9%; + + --ring: 0 0% 83.1%; + + --chart-1: 220 70% 50%; + + --chart-2: 160 60% 45%; + + --chart-3: 30 80% 55%; + + --chart-4: 280 65% 60%; + + --chart-5: 340 75% 55% + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + .typing-indicator { display: inline-block; animation: blink 1s step-end infinite;