This commit is contained in:
maojindao55
2025-02-12 19:22:13 +08:00
parent efa6d1792b
commit 3fac860583
7 changed files with 522 additions and 150 deletions

1
.dev.vars Normal file
View File

@@ -0,0 +1 @@
DASHSCOPE_API_KEY=sk-8d0715456b2844c7b28d1c4226fd0792

60
functions/api/chat.ts Normal file
View File

@@ -0,0 +1,60 @@
import OpenAI from 'openai';
export async function onRequestPost({ env, request }) {
try {
const { message } = await request.json();
const apiKey = env.DASHSCOPE_API_KEY;
if (!apiKey) {
throw new Error('API密钥未配置');
}
const openai = new OpenAI({
apiKey: apiKey,
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
});
// 使用流式响应
const stream = await openai.chat.completions.create({
model: "qwen-plus",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: message }
],
stream: true, // 启用流式响应
});
// 创建 ReadableStream
const readable = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
// 发送数据块
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ content })}\n\n`));
}
}
controller.close();
} catch (error) {
controller.error(error);
}
},
});
// 返回 SSE 流
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
return Response.json(
{ error: error.message },
{ status: 500 }
);
}
}

219
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.263.1",
"openai": "^4.83.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.6.0",
@@ -1787,11 +1788,19 @@
"version": "22.13.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.13.1.tgz",
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
"dev": true,
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -1836,6 +1845,28 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
@@ -1891,6 +1922,11 @@
"node": ">=10"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -2195,6 +2231,17 @@
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
@@ -2277,6 +2324,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz",
@@ -2355,6 +2410,14 @@
"node": ">=6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/execa": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/execa/-/execa-7.2.0.tgz",
@@ -2467,6 +2530,44 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/formdata-node/node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -2621,6 +2722,14 @@
"node": ">=14.18.0"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
@@ -2906,6 +3015,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -2943,8 +3071,7 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mz": {
"version": "2.7.0",
@@ -2977,7 +3104,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -3091,6 +3217,67 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "4.83.0",
"resolved": "https://registry.npmmirror.com/openai/-/openai-4.83.0.tgz",
"integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.75",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.75.tgz",
"integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/openai/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/openai/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/ora": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/ora/-/ora-6.3.1.tgz",
@@ -3931,6 +4118,11 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -3957,8 +4149,7 @@
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -4122,6 +4313,20 @@
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
@@ -4235,7 +4440,7 @@
"version": "3.24.2",
"resolved": "https://registry.npmmirror.com/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"dev": true,
"devOptional": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -17,6 +17,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.263.1",
"openai": "^4.83.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.6.0",

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Send, Menu, MoreHorizontal, UserPlus, UserMinus } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -42,11 +42,205 @@ const ChatUI = () => {
{ id: 5, name: "我" },
]);
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);
const [pendingContent, setPendingContent] = useState("");
const [isTyping, setIsTyping] = useState(false);
const typingSpeed = 30;
const currentMessageRef = useRef<number | null>(null);
const typewriterRef = useRef<NodeJS.Timeout | null>(null);
const accumulatedContentRef = useRef(""); // 用于跟踪完整内容
const abortController = new AbortController();
const handleRemoveUser = (userId: number) => {
setUsers(users.filter(user => user.id !== userId));
};
const typeWriter = (newContent: string, messageId: number) => {
if (!newContent) return;
setIsTyping(true);
currentMessageRef.current = messageId;
// 获取已显示的内容长度作为起始位置
const startIndex = accumulatedContentRef.current.length;
let currentIndex = startIndex;
// 清除之前的打字效果
if (typewriterRef.current) {
clearInterval(typewriterRef.current);
}
typewriterRef.current = setInterval(() => {
currentIndex++;
setMessages(prev => {
const newMessages = [...prev];
const messageIndex = newMessages.findIndex(msg => msg.id === messageId);
if (messageIndex !== -1) {
newMessages[messageIndex] = {
...newMessages[messageIndex],
content: newContent.slice(0, currentIndex)
};
}
return newMessages;
});
if (currentIndex >= newContent.length) {
if (typewriterRef.current) {
clearInterval(typewriterRef.current);
}
setIsTyping(false);
currentMessageRef.current = null;
accumulatedContentRef.current = newContent; // 更新完整内容
}
}, typingSpeed);
};
const handleSendMessage = async () => {
if (!inputMessage.trim()) return;
const userMessage = {
id: messages.length + 1,
sender: users[4],
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);
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 = '';
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;
});
// 滚动到底部
setTimeout(() => {
const chatContainer = document.querySelector('.scroll-area-viewport');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 0);
}
} 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);
}
}
} catch (error) {
console.error("发送消息失败:", error);
setMessages(prev => prev.map(msg =>
msg.id === aiMessage.id
? { ...msg, content: "错误: " + error.message, isError: true }
: msg
));
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
abortController.abort();
};
// 清理打字机效果
useEffect(() => {
return () => {
if (typewriterRef.current) {
clearInterval(typewriterRef.current);
}
};
}, []);
return (
<div className="h-screen flex flex-col bg-gray-100">
{/* Header */}
@@ -112,35 +306,36 @@ const ChatUI = () => {
<div className="flex-1 flex flex-col overflow-hidden">
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{/* Message 1 */}
<div className="flex items-start gap-3">
<Avatar>
<AvatarFallback style={{ backgroundColor: getAvatarData(users[0].name).backgroundColor, color: 'white' }}>
{users[0].name[0]}
</AvatarFallback>
</Avatar>
<div>
<div className="text-sm text-gray-500">{users[0].name}</div>
<div className="mt-1 bg-white p-3 rounded-lg shadow-sm">
{messages.map((message) => (
<div key={message.id}
className={`flex items-start gap-3 ${message.sender.name === "我" ? "justify-end" : ""}`}>
{message.sender.name !== "我" && (
<Avatar>
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
{message.sender.name[0]}
</AvatarFallback>
</Avatar>
)}
<div className={message.sender.name === "我" ? "text-right" : ""}>
<div className="text-sm text-gray-500">{message.sender.name}</div>
<div className={`mt-1 p-3 rounded-lg shadow-sm ${
message.sender.name === "我" ? "bg-blue-500 text-white" : "bg-white"
}`}>
{message.content}
{message.isAI && isTyping && currentMessageRef.current === message.id && (
<span className="typing-indicator ml-1"></span>
)}
</div>
</div>
{message.sender.name === "我" && (
<Avatar>
<AvatarFallback style={{ backgroundColor: getAvatarData(message.sender.name).backgroundColor, color: 'white' }}>
{message.sender.name[0]}
</AvatarFallback>
</Avatar>
)}
</div>
</div>
{/* Message 2 */}
<div className="flex items-start gap-3 justify-end">
<div className="text-right">
<div className="text-sm text-gray-500">{users[4].name}</div>
<div className="mt-1 bg-blue-500 text-white p-3 rounded-lg shadow-sm">
</div>
</div>
<Avatar>
<AvatarFallback style={{ backgroundColor: getAvatarData(users[4].name).backgroundColor, color: 'white' }}>
{users[4].name[0]}
</AvatarFallback>
</Avatar>
</div>
))}
</div>
</ScrollArea>
@@ -150,9 +345,19 @@ const ChatUI = () => {
<Input
placeholder="输入消息..."
className="flex-1"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
/>
<Button>
<Send className="w-4 h-4 mr-2" />
<Button
onClick={handleSendMessage}
disabled={isLoading}
>
{isLoading ? (
<div className="w-4 h-4 mr-2 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : (
<Send className="w-4 h-4 mr-2" />
)}
</Button>
</div>

View File

@@ -1,120 +1,12 @@
/* 基础样式重置 */
@tailwind base;
@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%
}
.typing-indicator {
display: inline-block;
animation: blink 1s step-end infinite;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
@keyframes blink {
50% { opacity: 0; }
}

View File

@@ -0,0 +1,8 @@
.typing-indicator {
display: inline-block;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}