diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss new file mode 100644 index 000000000..d5a7cd33d --- /dev/null +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss @@ -0,0 +1,139 @@ +.chatContainer { + display: flex; + flex-direction: column; +} + +.messageList { + flex: 1; + overflow-y: auto; + padding: 16px 12px 80px 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.userMessage { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.aiMessage { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 18px; + font-size: 15px; + line-height: 1.6; + word-break: break-word; + background: #fff; + color: #222; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.userMessage .bubble { + background: linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%); + color: #222; + border-bottom-right-radius: 6px; +} + +.aiMessage .bubble { + background: #fff; + color: #222; + border-bottom-left-radius: 6px; +} + +.time { + font-size: 11px; + color: #aaa; + margin: 4px 8px 0 8px; + align-self: flex-end; +} + +.inputBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + background: #fff; + padding: 10px 12px 10px 12px; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + z-index: 10; +} + +.input { + flex: 1; + border: none; + outline: none; + background: #f3f4f6; + border-radius: 18px; + padding: 10px 14px; + font-size: 15px; + margin-right: 8px; +} + +.sendButton { + background: var(--primary-gradient, linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)); + color: #fff; + border: none; + border-radius: 18px; + padding: 8px 18px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.sendButton:disabled { + background: #e5e7eb; + color: #aaa; + cursor: not-allowed; +} + +.iconBtn { + background: none; + border: none; + outline: none; + margin-right: 6px; + font-size: 20px; + color: #888; + cursor: pointer; + padding: 4px; + border-radius: 50%; + transition: background 0.2s, color 0.2s; +} + +.iconBtn:hover, .iconBtn:active { + background: #f3f4f6; + color: #5bbcff; +} + +.image { + max-width: 180px; + max-height: 180px; + border-radius: 10px; + display: block; +} + +.fileLink { + color: #5bbcff; + text-decoration: none; + font-size: 15px; + word-break: break-all; + display: flex; + align-items: center; +} + +.nav-title { + color: var(--primary-color); + font-weight: 700; + font-size: 18px; + text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx index 2727f4534..4e60367e5 100644 --- a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx @@ -1,8 +1,264 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; +import React, { useRef, useState, useEffect } from "react"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { + PictureOutlined, + PaperClipOutlined, + AudioOutlined, +} from "@ant-design/icons"; +import styles from "./AIAssistant.module.scss"; + +interface Message { + id: string; + content: string; + from: "user" | "ai"; + time: string; + type?: "text" | "image" | "file" | "audio"; + fileName?: string; + fileUrl?: string; +} + +const initialMessages: Message[] = [ + { + id: "1", + content: "你好!我是你的AI助手,有什么可以帮助你的吗?", + from: "ai", + time: "15:29", + type: "text", + }, +]; const AIAssistant: React.FC = () => { - return ; + const [messages, setMessages] = useState(initialMessages); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + const imageInputRef = useRef(null); + const [recognizing, setRecognizing] = useState(false); + const recognitionRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // 语音识别初始化 + useEffect(() => { + if (!("webkitSpeechRecognition" in window)) return; + const SpeechRecognition = (window as any).webkitSpeechRecognition; + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = false; + recognitionRef.current.lang = "zh-CN"; + recognitionRef.current.onresult = (event: any) => { + const transcript = event.results[0][0].transcript; + setInput((prev) => prev + transcript); + setRecognizing(false); + }; + recognitionRef.current.onerror = () => setRecognizing(false); + recognitionRef.current.onend = () => setRecognizing(false); + }, []); + + const handleSend = async () => { + if (!input.trim()) return; + const userMsg: Message = { + id: Date.now().toString(), + content: input, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }; + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setLoading(true); + setTimeout(() => { + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString() + "-ai", + content: "AI正在思考...(此处可接入真实API)", + from: "ai", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }, + ]); + setLoading(false); + }, 1200); + }; + + // 图片上传 + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: url, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "image", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 文件上传 + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: file.name, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "file", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 语音输入 + const handleVoiceInput = () => { + if (!recognitionRef.current) return alert("当前浏览器不支持语音输入"); + if (recognizing) { + recognitionRef.current.stop(); + setRecognizing(false); + } else { + recognitionRef.current.start(); + setRecognizing(true); + } + }; + + return ( + } loading={false}> + + + {messages.map((msg) => ( + + {msg.type === "text" && ( + {msg.content} + )} + {msg.type === "image" && ( + + + + )} + {msg.type === "file" && ( + + + + {msg.fileName} + + + )} + {/* 语音消息可后续扩展 */} + {msg.time} + + ))} + {loading && ( + + AI正在输入... + + )} + + + + imageInputRef.current?.click()} + title="图片" + type="button" + > + + + + fileInputRef.current?.click()} + title="文件" + type="button" + > + + + + + + + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSend(); + }} + disabled={loading} + /> + + 发送 + + + + + ); }; export default AIAssistant;