修复:AI问答模块
This commit is contained in:
@@ -1,269 +1,273 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { View, Textarea } from '@tarojs/components';
|
||||
// import Toast from '@tarojs/taro';
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {View, Textarea} from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import { User, ArrowUp } from '@nutui/icons-react-taro';
|
||||
import { sendAiMessage, stopAiMessage } from '@/api/ai';
|
||||
import { createWebSocket } from '@/utils/websocket';
|
||||
import {Button} from '@nutui/nutui-react-taro';
|
||||
import {User, ArrowUp} from '@nutui/icons-react-taro';
|
||||
import {sendAiMessage, stopAiMessage} from '@/api/ai';
|
||||
import {createWebSocket} from '@/utils/websocket';
|
||||
import './index.scss';
|
||||
import {APP_API_URL} from "@/utils/server";
|
||||
import Header from "../index/Header";
|
||||
import {WSS_API_URL} from "@/utils/server";
|
||||
|
||||
// 消息类型
|
||||
interface Message {
|
||||
id?: string;
|
||||
type?: 'user' | 'ai';
|
||||
query?: string;
|
||||
timestamp?: number;
|
||||
isTyping?: boolean;
|
||||
user?: string;
|
||||
responseMode?: string;
|
||||
id?: string;
|
||||
type?: 'user' | 'ai';
|
||||
query?: string;
|
||||
timestamp?: number;
|
||||
isTyping?: boolean;
|
||||
user?: string;
|
||||
responseMode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI问答页面
|
||||
*/
|
||||
const AiChat = () => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string>('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<any>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string>('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<any>(null);
|
||||
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
"当兵的注意事项?",
|
||||
"男兵应征报名对象:",
|
||||
"当兵公务员考试?",
|
||||
"请问您还有什么问题吗?"
|
||||
];
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
"当兵的注意事项?",
|
||||
"男兵应征报名对象:",
|
||||
"当兵公务员考试?",
|
||||
"请问您还有什么问题吗?"
|
||||
];
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 初始化WebSocket连接
|
||||
const initWebSocket = () => {
|
||||
const wsUrl = APP_API_URL.replace('https', 'ws') + '/chat/' + Taro.getStorageSync('UserId') || 'token';
|
||||
// const wsUrl = 'ws://127.0.0.1:9000/chat/test'
|
||||
wsRef.current = createWebSocket(wsUrl);
|
||||
|
||||
wsRef.current.onMessage((data: any) => {
|
||||
console.log('收到WebSocket消息:', data);
|
||||
|
||||
if (data.answer) {
|
||||
if (data.answer === '__END__') {
|
||||
// 消息结束,移除typing状态
|
||||
setMessages(prev => prev.map(msg =>
|
||||
msg.isTyping ? { ...msg, isTyping: false } : msg
|
||||
));
|
||||
setIsLoading(false);
|
||||
setCurrentTaskId('');
|
||||
} else {
|
||||
// 更新AI回复消息
|
||||
setMessages(prev => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
if (lastMessage && lastMessage.type === 'ai' && lastMessage.isTyping) {
|
||||
// 更新最后一条AI消息
|
||||
return prev.map((msg, index) =>
|
||||
index === prev.length - 1
|
||||
? { ...msg, query: msg.query + data.answer }
|
||||
: msg
|
||||
);
|
||||
} else {
|
||||
// 创建新的AI消息
|
||||
return [...prev, {
|
||||
id: Date.now().toString(),
|
||||
type: 'ai',
|
||||
query: data.answer,
|
||||
timestamp: Date.now(),
|
||||
isTyping: true
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
if (data.taskId) {
|
||||
setCurrentTaskId(data.taskId);
|
||||
}
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (!Taro.getStorageSync('UserId')) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
Taro.navigateTo({
|
||||
url: '/passport/sms-login',
|
||||
})
|
||||
return false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({behavior: 'smooth'});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 初始化WebSocket连接
|
||||
const initWebSocket = () => {
|
||||
// const wsUrl = APP_API_URL.replace('http', 'ws') + '/chat/' + Taro.getStorageSync('UserId') || 'token';
|
||||
console.log(WSS_API_URL + "/chat/" + Taro.getStorageSync('UserId'), 'wsUrl')
|
||||
wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + Taro.getStorageSync('UserId'));
|
||||
|
||||
wsRef.current.onMessage((data: any) => {
|
||||
console.log('收到WebSocket消息:', data);
|
||||
|
||||
if (data.answer) {
|
||||
if (data.answer === '__END__') {
|
||||
// 消息结束,移除typing状态
|
||||
setMessages(prev => prev.map(msg =>
|
||||
msg.isTyping ? {...msg, isTyping: false} : msg
|
||||
));
|
||||
setIsLoading(false);
|
||||
setCurrentTaskId('');
|
||||
} else {
|
||||
// 更新AI回复消息
|
||||
setMessages(prev => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
if (lastMessage && lastMessage.type === 'ai' && lastMessage.isTyping) {
|
||||
// 更新最后一条AI消息
|
||||
return prev.map((msg, index) =>
|
||||
index === prev.length - 1
|
||||
? {...msg, query: msg.query + data.answer}
|
||||
: msg
|
||||
);
|
||||
} else {
|
||||
// 创建新的AI消息
|
||||
return [...prev, {
|
||||
id: Date.now().toString(),
|
||||
type: 'ai',
|
||||
query: data.answer,
|
||||
timestamp: Date.now(),
|
||||
isTyping: true
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
if (data.taskId) {
|
||||
setCurrentTaskId(data.taskId);
|
||||
}
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
wsRef.current.onOpen(() => {
|
||||
console.log('WebSocket连接成功');
|
||||
});
|
||||
|
||||
wsRef.current.onError((error: any) => {
|
||||
console.error('WebSocket连接错误:', error);
|
||||
});
|
||||
|
||||
wsRef.current.connect().catch((error: any) => {
|
||||
console.error('WebSocket连接失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initWebSocket();
|
||||
Taro.hideTabBar();
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
wsRef.current.onOpen(() => {
|
||||
console.log('WebSocket连接成功');
|
||||
});
|
||||
// 发送消息
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!content.trim() || isLoading) return;
|
||||
|
||||
wsRef.current.onError((error: any) => {
|
||||
console.error('WebSocket连接错误:', error);
|
||||
// Toast.showToast({ title: '连接失败,请检查网络', icon: 'error'});
|
||||
});
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: 'user',
|
||||
query: content.trim(),
|
||||
timestamp: Date.now(),
|
||||
user: `${Taro.getStorageSync('UserId')}`
|
||||
};
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInputValue('');
|
||||
setIsLoading(true);
|
||||
|
||||
wsRef.current.connect().catch((error: any) => {
|
||||
console.error('WebSocket连接失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initWebSocket();
|
||||
Taro.hideTabBar();
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
try {
|
||||
await sendAiMessage({
|
||||
query: content.trim(),
|
||||
user: `${Taro.getStorageSync('UserId')}`,
|
||||
responseMode: 'streaming',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!content.trim() || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: 'user',
|
||||
query: content.trim(),
|
||||
timestamp: Date.now(),
|
||||
user: `${Taro.getStorageSync('UserId')}`
|
||||
// 停止AI回复
|
||||
const handleStopMessage = async () => {
|
||||
if (currentTaskId) {
|
||||
try {
|
||||
await stopAiMessage({taskId: currentTaskId});
|
||||
setIsLoading(false);
|
||||
setCurrentTaskId('');
|
||||
setMessages(prev => prev.map(msg =>
|
||||
msg.isTyping ? {...msg, isTyping: false} : msg
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('停止消息失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInputValue('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await sendAiMessage({
|
||||
query: content.trim(),
|
||||
user: `${Taro.getStorageSync('UserId')}`,
|
||||
responseMode: 'streaming',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
// Toast.showToast({ title: '发送失败,请重试', icon: 'error'});
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// 处理快捷问题点击
|
||||
const handleQuickQuestion = (question: string) => {
|
||||
handleSendMessage(question);
|
||||
};
|
||||
|
||||
// 停止AI回复
|
||||
const handleStopMessage = async () => {
|
||||
if (currentTaskId) {
|
||||
try {
|
||||
await stopAiMessage({ taskId: currentTaskId });
|
||||
setIsLoading(false);
|
||||
setCurrentTaskId('');
|
||||
setMessages(prev => prev.map(msg =>
|
||||
msg.isTyping ? { ...msg, isTyping: false } : msg
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('停止消息失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理快捷问题点击
|
||||
const handleQuickQuestion = (question: string) => {
|
||||
console.log(question,'qqq')
|
||||
handleSendMessage(question);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="ai-chat">
|
||||
{/*<View className="chat-header">*/}
|
||||
{/* AI智能问答*/}
|
||||
{/*</View>*/}
|
||||
|
||||
<View className="chat-container">
|
||||
<View className="messages-container">
|
||||
{messages.length === 0 ? (
|
||||
<View className="empty-state">
|
||||
<View className="empty-icon">🤖</View>
|
||||
<View className="empty-title">您好!我是AI助手</View>
|
||||
<View className="empty-desc">
|
||||
我可以为您解答各种问题,请输入您想了解的内容
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<View key={message.id} className={`message ${message.type}`}>
|
||||
<View className={`avatar ${message.type}-avatar`}>
|
||||
{message.type === 'user' ? <User size={20} /> : '🤖'}
|
||||
return (
|
||||
<View className="ai-chat">
|
||||
<Header/>
|
||||
<View className="chat-container">
|
||||
<View className="messages-container">
|
||||
{messages.length === 0 ? (
|
||||
<View className="empty-state">
|
||||
<View className="empty-icon">🤖</View>
|
||||
<View className="empty-title">您好!我是AI助手</View>
|
||||
<View className="empty-desc">
|
||||
我可以为您解答各种问题,请输入您想了解的内容
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<View key={message.id} className={`message ${message.type}`}>
|
||||
<View className={`avatar ${message.type}-avatar`}>
|
||||
{message.type === 'user' ? <User size={20}/> : '🤖'}
|
||||
</View>
|
||||
<View className="message-content">
|
||||
{message.isTyping ? (
|
||||
<View className="typing-indicator">
|
||||
<View className="dot"></View>
|
||||
<View className="dot"></View>
|
||||
<View className="dot"></View>
|
||||
</View>
|
||||
) : (
|
||||
message.query
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
<View ref={messagesEndRef}/>
|
||||
</View>
|
||||
<View className="message-content">
|
||||
{message.isTyping ? (
|
||||
<View className="typing-indicator">
|
||||
<View className="dot"></View>
|
||||
<View className="dot"></View>
|
||||
<View className="dot"></View>
|
||||
|
||||
{messages.length === 0 && (
|
||||
<View className="quick-questions">
|
||||
<View className="quick-title">
|
||||
💡 您可以问我:
|
||||
</View>
|
||||
<View className="questions-list">
|
||||
{quickQuestions.map((question, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="question-item"
|
||||
onClick={() => handleQuickQuestion(question)}
|
||||
>
|
||||
{question}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
message.query
|
||||
)}
|
||||
)}
|
||||
|
||||
<View className="input-container">
|
||||
<View className="input-wrapper">
|
||||
<Textarea
|
||||
className="message-input"
|
||||
value={inputValue}
|
||||
placeholder="请输入您的问题..."
|
||||
onInput={(e) => setInputValue(e.detail.value)}
|
||||
onConfirm={() => handleSendMessage(inputValue)}
|
||||
autoHeight
|
||||
maxlength={500}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<Button
|
||||
onClick={handleStopMessage}
|
||||
type="danger"
|
||||
icon={<ArrowUp/>}
|
||||
>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="danger"
|
||||
onClick={() => handleSendMessage(inputValue)}
|
||||
// disabled={!inputValue.trim()}
|
||||
icon={<ArrowUp/>}
|
||||
>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
<View ref={messagesEndRef} />
|
||||
</View>
|
||||
|
||||
{messages.length === 0 && (
|
||||
<View className="quick-questions">
|
||||
<View className="quick-title">
|
||||
💡 您可以问我:
|
||||
</View>
|
||||
<View className="questions-list">
|
||||
{quickQuestions.map((question, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="question-item"
|
||||
onClick={() => handleQuickQuestion(question)}
|
||||
>
|
||||
{question}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="input-container">
|
||||
<View className="input-wrapper">
|
||||
<Textarea
|
||||
className="message-input"
|
||||
value={inputValue}
|
||||
placeholder="请输入您的问题..."
|
||||
onInput={(e) => setInputValue(e.detail.value)}
|
||||
onConfirm={() => handleSendMessage(inputValue)}
|
||||
disabled={isLoading}
|
||||
autoHeight
|
||||
maxlength={500}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<Button
|
||||
onClick={handleStopMessage}
|
||||
type="danger"
|
||||
icon={<ArrowUp />}
|
||||
>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="danger"
|
||||
onClick={() => handleSendMessage(inputValue)}
|
||||
disabled={!inputValue.trim()}
|
||||
icon={<ArrowUp />}
|
||||
>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default AiChat;
|
||||
|
||||
Reference in New Issue
Block a user