This commit is contained in:
maojindao55
2025-02-12 20:21:47 +08:00
parent 3fac860583
commit b7852db708
5 changed files with 246 additions and 114 deletions

1
devrun.sh Normal file
View File

@@ -0,0 +1 @@
wrangler pages dev -- npm run dev

View File

@@ -2,7 +2,7 @@ import OpenAI from 'openai';
export async function onRequestPost({ env, request }) { export async function onRequestPost({ env, request }) {
try { try {
const { message } = await request.json(); const { message, personality } = await request.json();
const apiKey = env.DASHSCOPE_API_KEY; const apiKey = env.DASHSCOPE_API_KEY;
if (!apiKey) { if (!apiKey) {
@@ -14,14 +14,34 @@ export async function onRequestPost({ env, request }) {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" 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({ const stream = await openai.chat.completions.create({
model: "qwen-plus", model: "qwen-plus",
messages: [ messages: [
{ role: "system", content: "You are a helpful assistant." }, { role: "system", content: systemPrompt },
{ role: "user", content: message } { role: "user", content: message }
], ],
stream: true, // 启用流式响应 stream: true,
temperature: personality === "high_eq" ? 0.7 : 0.9, // 高情商用较低温度保持稳定,低情商用较高温度增加随机性
}); });
// 创建 ReadableStream // 创建 ReadableStream

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your App</title> <title>AI机器人群聊</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -34,17 +34,19 @@ const getAvatarData = (name: string) => {
}; };
const ChatUI = () => { const ChatUI = () => {
// 添加 AI 角色定义
const aiCharacters = [
{ id: 'ai1', name: "暖心姐", personality: "high_eq" },
{ id: 'ai2', name: "直男哥", personality: "low_eq" }
];
const [users, setUsers] = useState([ const [users, setUsers] = useState([
{ id: 1, name: "张三" }, { id: 1, name: "" },
{ id: 2, name: "李四" }, ...aiCharacters
{ id: 3, name: "王五" },
{ id: 4, name: "赵六" },
{ id: 5, name: "我" },
]); ]);
const [showMembers, setShowMembers] = useState(false); const [showMembers, setShowMembers] = useState(false);
const [messages, setMessages] = useState([ 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 [inputMessage, setInputMessage] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -54,6 +56,7 @@ const ChatUI = () => {
const currentMessageRef = useRef<number | null>(null); const currentMessageRef = useRef<number | null>(null);
const typewriterRef = useRef<NodeJS.Timeout | null>(null); const typewriterRef = useRef<NodeJS.Timeout | null>(null);
const accumulatedContentRef = useRef(""); // 用于跟踪完整内容 const accumulatedContentRef = useRef(""); // 用于跟踪完整内容
const messagesEndRef = useRef<HTMLDivElement>(null);
const abortController = new AbortController(); const abortController = new AbortController();
@@ -102,27 +105,42 @@ const ChatUI = () => {
}, typingSpeed); }, typingSpeed);
}; };
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = async () => { const handleSendMessage = async () => {
if (!inputMessage.trim()) return; if (!inputMessage.trim()) return;
// 添加用户消息
const userMessage = { const userMessage = {
id: messages.length + 1, id: messages.length + 1,
sender: users[4], sender: users[0],
content: inputMessage, content: inputMessage,
isAI: false isAI: false
}; };
setMessages(prev => [...prev, userMessage]); setMessages(prev => [...prev, userMessage]);
setInputMessage("");
setIsLoading(true);
setPendingContent("");
accumulatedContentRef.current = "";
// 依次请求两个 AI 的回复
for (let i = 0; i < aiCharacters.length; i++) {
// 创建当前 AI 角色的消息
const aiMessage = { const aiMessage = {
id: messages.length + 2, id: messages.length + 2 + i,
sender: { id: 0, name: "AI助手" }, sender: { id: aiCharacters[i].id, name: aiCharacters[i].name },
content: "", content: "",
isAI: true isAI: true
}; };
setMessages(prev => [...prev, aiMessage]);
setInputMessage(""); // 添加当前 AI 的消息
setIsLoading(true); setMessages(prev => [...prev, aiMessage]);
try { try {
const response = await fetch('/api/chat', { const response = await fetch('/api/chat', {
@@ -132,6 +150,7 @@ const ChatUI = () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
message: inputMessage, message: inputMessage,
personality: aiCharacters[i].personality,
}), }),
}); });
@@ -147,16 +166,15 @@ const ChatUI = () => {
} }
let buffer = ''; let buffer = '';
let completeResponse = ''; // 用于跟踪完整的响应
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
// 解码并添加到buffer
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
// 处理buffer中的完整SSE消息
let newlineIndex; let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) >= 0) { while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIndex); const line = buffer.slice(0, newlineIndex);
@@ -166,26 +184,18 @@ const ChatUI = () => {
try { try {
const data = JSON.parse(line.slice(6)); const data = JSON.parse(line.slice(6));
if (data.content) { if (data.content) {
// 使用函数式更新确保状态更新正确 completeResponse += data.content;
setMessages(prev => { setMessages(prev => {
const newMessages = [...prev]; const newMessages = [...prev];
const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id); const aiMessageIndex = newMessages.findIndex(msg => msg.id === aiMessage.id);
if (aiMessageIndex !== -1) { if (aiMessageIndex !== -1) {
newMessages[aiMessageIndex] = { newMessages[aiMessageIndex] = {
...newMessages[aiMessageIndex], ...newMessages[aiMessageIndex],
content: newMessages[aiMessageIndex].content + data.content content: completeResponse
}; };
} }
return newMessages; return newMessages;
}); });
// 滚动到底部
setTimeout(() => {
const chatContainer = document.querySelector('.scroll-area-viewport');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 0);
} }
} catch (e) { } catch (e) {
console.error('解析响应数据失败:', e); console.error('解析响应数据失败:', e);
@@ -194,26 +204,9 @@ const ChatUI = () => {
} }
} }
// 处理剩余的buffer // 等待一小段时间再开始下一个 AI 的回复
if (buffer.length > 0 && buffer.startsWith('data: ')) { if (i < aiCharacters.length - 1) {
try { await new Promise(resolve => setTimeout(resolve, 1000));
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);
}
} }
} catch (error) { } catch (error) {
@@ -223,9 +216,10 @@ const ChatUI = () => {
? { ...msg, content: "错误: " + error.message, isError: true } ? { ...msg, content: "错误: " + error.message, isError: true }
: msg : msg
)); ));
} finally {
setIsLoading(false);
} }
}
setIsLoading(false);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -248,7 +242,7 @@ const ChatUI = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Menu className="w-6 h-6" /> <Menu className="w-6 h-6" />
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -336,6 +330,7 @@ const ChatUI = () => {
)} )}
</div> </div>
))} ))}
<div ref={messagesEndRef} />
</div> </div>
</ScrollArea> </ScrollArea>

View File

@@ -2,6 +2,122 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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 { .typing-indicator {
display: inline-block; display: inline-block;
animation: blink 1s step-end infinite; animation: blink 1s step-end infinite;