newAi.jsx 11.4 KB
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { AudioOutline } from "antd-mobile-icons";
import './AiChatStyles.less'; // 引入外部样式文件

const ChatInterface = () => {
  // ==================== 状态管理 ====================
  const [sessionId, setSessionId] = useState('');
  const [sUserId] = useState('user-001');
  const [sUserType] = useState('sysadmin');
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [currentModel, setCurrentModel] = useState('general');
  const [chatHistory, setChatHistory] = useState([]);
  const [welcomeContent, setWelcomeContent] = useState('');
  
  const messagesEndRef = useRef(null);
  const inputRef = useRef(null);

  // ==================== 配置 ====================
  const CONFIG = {
    backendUrl: 'http://localhost:8099/xlyAi',
    endpoints: {
      chat: '/api/v1/chat/query',
      process: '/api/v1/chat/query',
    },
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    maxHistory: 20,
  };

  // ==================== 工具函数 ====================
  const getCurrentTime = () => {
    const now = new Date();
    return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  };

  const generateRandomString = (length) => {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    for (let i = 0; i < length; i++) {
      result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
  };

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  // ==================== 初始化 ====================
  useEffect(() => {
    setMessages([
      {
        id: 'welcome',
        type: 'ai',
        content: '',
        time: getCurrentTime(),
        isWelcome: true
      }
    ]);

    const initSession = async () => {
      try {
        const initUrl = `${CONFIG.backendUrl}/api/v1/chat/init?sUserId=${sUserId}&sUserType=${sUserType}`;
        const response = await fetch(initUrl, {
          method: 'POST',
          headers: CONFIG.headers,
          body: JSON.stringify({})
        });
        const data = await response.json();
        if (data.data) {
          setWelcomeContent(data.data);
          setMessages(prev => prev.map(msg => 
            msg.id === 'welcome' ? { ...msg, content: data.data } : msg
          ));
        }
      } catch (error) {
        console.error('初始化失败:', error);
      }
    };

    initSession();
    inputRef.current?.focus();

    const handleKeyDown = (e) => {
      if (e.ctrlKey && e.key === 'Enter') {
        handleSendMessage();
      }
      if (e.key === 'Escape') {
        setInputValue('');
      }
      if (e.key === 'ArrowUp' && inputValue === '') {
        const lastUserMessage = chatHistory
          .filter(item => item.role === 'user')
          .pop();
        if (lastUserMessage) {
          setInputValue(lastUserMessage.content);
          e.preventDefault();
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);

  useEffect(() => {
    scrollToBottom();
  }, [messages, isLoading]);

  // ==================== 消息处理 ====================
  const addMessage = (content, type, isError = false) => {
    const newMessage = {
      id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
      type,
      content,
      time: getCurrentTime(),
      isError
    };
    setMessages(prev => [...prev, newMessage]);
    return newMessage.id;
  };

  const handleSendMessage = async () => {
    const message = inputValue.trim();
    if (!message || isLoading) return;

    let currentSessionId = sessionId;
    if (!currentSessionId) {
      currentSessionId = generateRandomString(20);
      setSessionId(currentSessionId);
    }

    setInputValue('');
    setIsLoading(true);

    addMessage(message, 'user');

    const newHistoryItem = { role: 'user', content: message, timestamp: Date.now() };
    setChatHistory(prev => {
      const updated = [...prev, newHistoryItem];
      return updated.length > CONFIG.maxHistory ? updated.slice(-CONFIG.maxHistory) : updated;
    });

    try {
      const endpoint = currentModel === 'process' ? CONFIG.endpoints.process : CONFIG.endpoints.chat;
      const requestData = {
        message,
        modelType: currentModel,
        sUserId,
        sUserType,
        sessionId: currentSessionId
      };

      const response = await fetch(`${CONFIG.backendUrl}${endpoint}`, {
        method: 'POST',
        headers: CONFIG.headers,
        body: JSON.stringify(requestData)
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();
      
      if (data.data) {
        addMessage(data.data, 'ai');
        setChatHistory(prev => {
          const updated = [...prev, { role: 'assistant', content: data.data, timestamp: Date.now() }];
          return updated.length > CONFIG.maxHistory ? updated.slice(-CONFIG.maxHistory) : updated;
        });
      }
    } catch (error) {
      console.error('请求失败:', error);
      const errorMessage = `
抱歉,请求出现错误:${error.message}

**可能的原因:**
1. Spring Boot 后端服务未启动
2. API 接口路径不正确
3. 网络连接问题

**检查步骤:**
1. 确保后端服务在端口 8099 运行
2. 检查浏览器控制台查看详细错误
3. 刷新页面重试
      `;
      addMessage(errorMessage, 'ai', true);
    } finally {
      setIsLoading(false);
      inputRef.current?.focus();
    }
  };

  const handleClearChat = () => {
    if (window.confirm('确定要清空当前对话吗?')) {
      setMessages([
        {
          id: 'cleared',
          type: 'ai',
          content: '对话已清空,请开始新的对话。',
          time: getCurrentTime(),
        }
      ]);
      setChatHistory([]);
      setSessionId('');
    }
  };

  const handleCopyMessage = (content) => {
    navigator.clipboard.writeText(content).then(() => {
      alert('已复制到剪贴板');
    });
  };

  const handleRegenerateMessage = (messageId, content) => {
    setChatHistory(prev => prev.filter(item => 
      !(item.role === 'assistant' && item.content === content)
    ));
    
    setMessages(prev => prev.filter(msg => msg.id !== messageId));
    
    setInputValue(content);
    setTimeout(() => handleSendMessage(), 100);
  };

  const handleModelChange = (e) => {
    setCurrentModel(e.target.value);
  };

  // ==================== 渲染 ====================
  return (
    <div className="ai-chat-container">
      {/* 头部 */}
      <div className="chat-header">
        <div className="header-left">
          <h1>小羚羊Ai-agent智能体</h1>
          <p>AI 印刷助手</p>
        </div>
        <div className="header-right">
          <select 
            className="model-selector"
            value={currentModel}
            onChange={handleModelChange}
          >
            <option value="process">小羚羊印刷行业大模型</option>
            <option value="general">qwen2.5:14b</option>
          </select>
          <button 
            className="model-selectors"
            onClick={handleClearChat}
          >
            清空对话
          </button>
        </div>
      </div>

      {/* 主体 */}
      <div className="chat-body">
        <div className="chat-main">
          {/* 消息区域 */}
          <div className="messages-container">
            {messages.map((msg) => (
              <div 
                key={msg.id}
                className={`message ${msg.type}-message`}
              >
                <div className={`message-bubble ${msg.type}-bubble`}>
                  <div className="message-content">
                    {msg.type === 'ai' ? (
                      <ReactMarkdown 
                        remarkPlugins={[remarkGfm]}
                        components={{
                          code: ({ node, inline, className, children, ...props }) => (
                            inline ? (
                              <code className="inline-code" {...props}>
                                {children}
                              </code>
                            ) : (
                              <pre className="code-block">
                                <code {...props}>{children}</code>
                              </pre>
                            )
                          )
                        }}
                      >
                        {msg.content || welcomeContent}
                      </ReactMarkdown>
                    ) : (
                      msg.content
                    )}
                  </div>
                  <div className="message-meta">
                    <span className="message-time">{msg.time}</span>
                    {msg.type === 'ai' && !msg.isWelcome && (
                      <div className="message-actions">
                        <button 
                          className="action-btn"
                          onClick={() => handleCopyMessage(msg.content)}
                        >
                          复制
                        </button>
                        <button 
                          className="action-btn"
                          onClick={() => handleRegenerateMessage(msg.id, msg.content)}
                        >
                          重新生成
                        </button>
                      </div>
                    )}
                  </div>
                </div>
              </div>
            ))}
            
            {/* 打字机效果 */}
            {isLoading && (
              <div className="message ai-message">
                <div className="typing-indicator">
                  <div className="typing-dot"></div>
                  <div className="typing-dot" style={{ animationDelay: '0.2s' }}></div>
                  <div className="typing-dot" style={{ animationDelay: '0.4s' }}></div>
                  <span className="typing-text">正在思考...</span>
                </div>
              </div>
            )}
            
            <div ref={messagesEndRef} className="bottom-spacer" />
          </div>

          {/* 输入区域 */}
          <div className="input-section">
            <div className="input-wrapper">
              <input
                ref={inputRef}
                type="text"
                className="message-input"
                placeholder="输入您的问题..."
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
                onKeyPress={(e) => {
                  if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    handleSendMessage();
                  }
                }}
                disabled={isLoading}
              />
              <AudioOutline className='message-icon'/>
              <button 
                className={`send-button ${isLoading ? 'disabled' : ''}`}
                onClick={handleSendMessage}
                disabled={isLoading}
              >
                发送
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default ChatInterface;