From 3fac86058382cce1a60c8911af858006733fc2cf Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Wed, 12 Feb 2025 19:22:13 +0800 Subject: [PATCH] add --- .dev.vars | 1 + functions/api/chat.ts | 60 ++++++++ package-lock.json | 219 +++++++++++++++++++++++++- package.json | 1 + src/components/ChatUI.tsx | 265 ++++++++++++++++++++++++++++---- src/index.css | 118 +------------- src/styles/typing-indicator.css | 8 + 7 files changed, 522 insertions(+), 150 deletions(-) create mode 100644 .dev.vars create mode 100644 functions/api/chat.ts create mode 100644 src/styles/typing-indicator.css diff --git a/.dev.vars b/.dev.vars new file mode 100644 index 0000000..4de4a74 --- /dev/null +++ b/.dev.vars @@ -0,0 +1 @@ +DASHSCOPE_API_KEY=sk-8d0715456b2844c7b28d1c4226fd0792 \ No newline at end of file diff --git a/functions/api/chat.ts b/functions/api/chat.ts new file mode 100644 index 0000000..8b025a2 --- /dev/null +++ b/functions/api/chat.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1c2a224..92beda1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index 5c1647d..c37d311 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ChatUI.tsx b/src/components/ChatUI.tsx index a67699b..fd2fd5b 100644 --- a/src/components/ChatUI.tsx +++ b/src/components/ChatUI.tsx @@ -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(null); + const typewriterRef = useRef(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 (
{/* Header */} @@ -112,35 +306,36 @@ const ChatUI = () => {
- {/* Message 1 */} -
- - - {users[0].name[0]} - - -
-
{users[0].name}
-
- 大家好! + {messages.map((message) => ( +
+ {message.sender.name !== "我" && ( + + + {message.sender.name[0]} + + + )} +
+
{message.sender.name}
+
+ {message.content} + {message.isAI && isTyping && currentMessageRef.current === message.id && ( + + )} +
+ {message.sender.name === "我" && ( + + + {message.sender.name[0]} + + + )}
-
- - {/* Message 2 */} -
-
-
{users[4].name}
-
- 你好! -
-
- - - {users[4].name[0]} - - -
+ ))}
@@ -150,9 +345,19 @@ const ChatUI = () => { setInputMessage(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} /> -
diff --git a/src/index.css b/src/index.css index 23ee7c3..215ce6f 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } } diff --git a/src/styles/typing-indicator.css b/src/styles/typing-indicator.css new file mode 100644 index 0000000..26e1900 --- /dev/null +++ b/src/styles/typing-indicator.css @@ -0,0 +1,8 @@ +.typing-indicator { + display: inline-block; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + 50% { opacity: 0; } +} \ No newline at end of file