aiComponent.jsx 9.12 KB
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from "react";
import { SideBar, Input, Button, List, Space, Popup, ActionSheet, Toast } from "antd-mobile";
import { UploadOutline, CloseOutline, CopyOutline, ReloadOutline, MoreOutline } from "antd-mobile-icons";
import styles from "./index.less"; // 引入外部样式

const AIAssistant = () => {
  const [messages, setMessages] = useState([
    { id: 1, type: 'ai', content: '欢迎使用小羚羊AI印刷助手!我是专业的印刷行业智能顾问,可以帮助您解答印刷相关问题。', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }
  ]);

  const [inputValue, setInputValue] = useState('');
  const [currentModel, setCurrentModel] = useState('process');
  const [showSidebar, setShowSidebar] = useState(false);
  const [showActionSheet, setShowActionSheet] = useState(false);
  const [selectedMessageId, setSelectedMessageId] = useState(null);
  const messagesEndRef = useRef(null);

  // 预设问题列表
  const presetQuestions = [
    "如何选择合适的印刷材料?",
    "印刷色彩搭配建议",
    "印刷工艺选择指南",
    "印刷成本优化方案",
    "印刷质量控制要点"
  ];

  // 模型选项
  const modelOptions = [
    { label: '小羚羊印刷行业大模型', value: 'process' },
    { label: '通用模型 qwen2.5:14b', value: 'general' }
  ];


  const scrollToBottom = useCallback(() => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({
        behavior: 'auto', // 流式中用 instant 更流畅
        block: 'end'
      });
    }
  }, []);

  // 节流版本(每 100ms 最多一次)
  const throttledScroll = useRef(null);

  useLayoutEffect(() => {
    if (throttledScroll.current) clearTimeout(throttledScroll.current);
    throttledScroll.current = setTimeout(() => {
      scrollToBottom();
    }, 50); // 流式中可设为 50ms,平衡流畅与性能
  }, [messages, streamingMessage]);

  // 在组件内部添加新的状态
  const [streamingMessage, setStreamingMessage] = useState(null); // { id, content, fullContent, time }

  // 发送
  const sendMessage = async () => {
    if (!inputValue.trim()) return;

    const userMessage = {
      id: Date.now(),
      type: 'user',
      content: inputValue,
      time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    };

    setMessages(prev => [...prev, userMessage]);
    setInputValue('');

    // 模拟 AI 的完整回复内容
    const fullResponse = `关于"${inputValue}"的问题,根据${currentModel === 'process' ? '印刷行业专业模型' : '通用模型'}的分析,这是相关的专业建议...`;

    const aiId = Date.now() + 1;
    const startTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

    // 开始流式输出:逐字显示
    let index = 0;
    const interval = setInterval(() => {
      setStreamingMessage({
        id: aiId,
        type: 'ai',
        content: fullResponse.slice(0, index + 1),
        fullContent: fullResponse,
        time: startTime
      });
      index++;

      if (index >= fullResponse.length) {
        clearInterval(interval);
        // 流式结束,合并到正式 messages
        setTimeout(() => {
          setMessages(prev => [
            ...prev.filter(msg => msg.id !== aiId), // 防止重复(如果之前已插入占位)
            {
              id: aiId,
              type: 'ai',
              content: fullResponse,
              time: startTime
            }
          ]);
          setStreamingMessage(null);
        }, 100);
      }
    }, 30); // 每30ms输出一个字符(可调整速度)
  };
  // 处理回车发送
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      sendMessage();
    }
  };

  // 复制消息
  const copyMessage = (content) => {
    navigator.clipboard.writeText(content).then(() => {
      Toast.show({ content: '已复制到剪贴板', duration: 1000 });
    });
  };

  // 重新生成
  const regenerateMessage = (content) => {
    setInputValue(content);
    setShowActionSheet(false);
  };

  // 清空对话
  const clearChat = () => {
    setMessages([]);
    Toast.show({ content: '对话已清空', duration: 1000 });
  };

  // 选择预设问题
  const selectPresetQuestion = (question) => {
    setInputValue(question);
    setShowSidebar(false);
  };

  return (
    <div className={styles.container}>
      {/* 顶部栏 */}
      <div className={styles.header}>
        <div>
          <div className={styles.headerTitle}>小羚羊AI智能体</div>
          <div className={styles.headerSubtitle}>印刷行业专业助手({modelOptions.find(x=>x.value === currentModel).label})</div>
        </div>
        <Space>
          <Button
            fill="none"
            color="white"
            onClick={() => setShowSidebar(true)}
            className={styles.headerButton}
          >
            <MoreOutline />
          </Button>
        </Space>
      </div>

      {/* 消息列表 */}
      {/* 消息列表 */}
      <div className={styles.messageContainer}>
        <List>
          {/* 已完成的消息 */}
          {messages.map((message) => (
            <List.Item key={message.id}>
              <div className={`${styles.messageItem} ${message.type === 'user' ? styles.userMessage : styles.aiMessage}`}>
                <div className={`${styles.messageBubble} ${message.type === 'user' ? styles.userBubble : styles.aiBubble}`}>
                  <div className={styles.messageContent}>
                    {message.content}
                  </div>
                  <div className={styles.messageMeta}>
                    <span>{message.time}</span>
                    {message.type === 'ai' && (
                      <Button
                        fill="none"
                        size="small"
                        onClick={() => {
                          setSelectedMessageId(message.id);
                          setShowActionSheet(true);
                        }}
                        className={styles.actionButton}
                      >
                        <MoreOutline />
                      </Button>
                    )}
                  </div>
                </div>
              </div>
            </List.Item>
          ))}

          {/* 正在流式输出的消息(如果有) */}
          {streamingMessage && (
            <List.Item key={streamingMessage.id}>
              <div className={`${styles.messageItem} ${styles.aiMessage}`}>
                <div className={`${styles.messageBubble} ${styles.aiBubble}`}>
                  <div className={styles.messageContent}>
                    {streamingMessage.content}
                    <span className={styles.cursor}>|</span> {/* 可选:加光标动画 */}
                  </div>
                  <div className={styles.messageMeta}>
                    <span>{streamingMessage.time}</span>
                    {/* 流式中不显示操作按钮 */}
                  </div>
                </div>
              </div>
            </List.Item>
          )}

          <div ref={messagesEndRef} />
        </List>
      </div>

      {/* 输入区域 */}
      <div className={styles.inputContainer}>
        <div className={styles.inputWrapper}>
          <Input
            value={inputValue}
            onChange={setInputValue}
            placeholder="输入您的问题..."
            onKeyDown={handleKeyDown}
            className={styles.inputField}
          />
          <Button
            color="primary"
            fill="solid"
            onClick={sendMessage}
            disabled={!inputValue.trim()}
            className={styles.sendButton}
          >
            <UploadOutline />
          </Button>
        </div>
      </div>

      {/* 侧边栏 - 预设问题 */}
      <Popup
        visible={showSidebar}
        onMaskClick={() => setShowSidebar(false)}
        position="left"
        destroyOnClose
      >
        <div className={styles.sidebarContainer}>
          <div className={styles.sidebarHeader}>
            <h3 className={styles.sidebarTitle}>设置</h3>
          </div>
          <List>
            {/* {presetQuestions.map((question, index) => (
              <List.Item
                key={index}
                onClick={() => selectPresetQuestion(question)}
                className={styles.presetItem}
              >
                {question}
              </List.Item>
            ))} */}
            {
              modelOptions.map((item, index) => {
                return (
                  <List.Item
                    key={index}
                    onClick={() => setCurrentModel(item.value)}
                    className={styles.presetItem}
                  >
                    {item.label}
                  </List.Item>
                );
              })
            }
            <List.Item
              onClick={clearChat}
              className={styles.clearItem}
            >
              <Space align="center">
                <CloseOutline />
                清空对话
              </Space>
            </List.Item>
          </List>
        </div>
      </Popup>
    </div>
  );
};

export default AIAssistant;