| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- import React, { useState, useRef, useEffect } from 'react';
- import {
- PlusOutlined,
- CopyOutlined,
- LikeOutlined,
- DislikeOutlined,
- ArrowLeftOutlined,
- PaperClipOutlined,
- PhoneOutlined,
- } from '@ant-design/icons';
- import { Tooltip, message } from 'antd';
- import ReactMarkdown from 'react-markdown';
- import { useChatStore, Message } from '../store/chatStore';
- import { sendChatMessage } from '../api';
- import '../styles/index.scss';
- import aiIcon from '@/assets/public/aiIcon.png';
- export const ChatInterface: React.FC = () => {
- const {
- currentSessionId,
- sessions,
- selectedAppId,
- addMessage,
- addSession,
- updateCurrentSession,
- loading,
- setLoading,
- } = useChatStore();
- const [inputValue, setInputValue] = useState('');
- const [activeFeatures, setActiveFeatures] = useState<Set<string>>(new Set());
- const [abortController, setAbortController] = useState<AbortController | null>(null);
- const messagesEndRef = useRef<HTMLDivElement>(null);
- const inputRef = useRef<HTMLDivElement>(null);
- const currentSession = sessions.find((s) => s.id === currentSessionId);
- const messages = currentSession?.messages || [];
- useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [messages]);
- const handleSendMessage = async () => {
- if (!inputValue.trim() || loading) return;
- const userMessage: Message = {
- id: Date.now().toString(),
- role: 'user',
- content: inputValue.trim(),
- createTime: new Date().toISOString(),
- };
- let sessionId = currentSessionId;
- if (!sessionId) {
- const newSession = {
- id: Date.now().toString(),
- topic: inputValue.trim().slice(0, 20),
- appId: selectedAppId || undefined,
- messages: [userMessage],
- createTime: new Date().toISOString(),
- };
- addSession(newSession);
- sessionId = newSession.id;
- setInputValue('');
- return;
- }
- addMessage(sessionId, userMessage);
- const userText = inputValue.trim();
- setInputValue('');
- setLoading(true);
- const controller = new AbortController();
- setAbortController(controller);
- try {
- const assistantMessageId = (Date.now() + 1).toString();
- addMessage(sessionId, {
- id: assistantMessageId,
- role: 'assistant',
- content: '',
- createTime: new Date().toISOString(),
- });
- const response = await sendChatMessage({
- message: userText,
- appId: currentSession?.appId || selectedAppId || undefined,
- sessionId,
- });
- updateCurrentSession((session) => {
- const lastMsgIndex = session.messages.length - 1;
- if (lastMsgIndex >= 0 && session.messages[lastMsgIndex].id === assistantMessageId) {
- session.messages[lastMsgIndex].content = response || '抱歉,我暂时无法回答这个问题。';
- }
- });
- } catch (error: any) {
- if (error.name === 'AbortError') {
- message.info('请求已取消');
- } else {
- message.error('发送失败,请重试');
- }
- } finally {
- setLoading(false);
- setAbortController(null);
- }
- };
- const handleCancel = () => {
- abortController?.abort();
- setLoading(false);
- };
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- handleSendMessage();
- }
- };
- const handleCopy = (content: string) => {
- navigator.clipboard.writeText(content);
- message.success('复制成功');
- };
- const toggleFeature = (feature: string) => {
- setActiveFeatures((prev) => {
- const next = new Set(prev);
- if (next.has(feature)) {
- next.delete(feature);
- } else {
- next.add(feature);
- }
- return next;
- });
- };
- const handleSuggestionClick = (text: string) => {
- setInputValue(text);
- inputRef.current?.focus();
- };
- const handleBack = () => {
- window.close();
- setTimeout(() => {
- window.location.href = '/appCenter';
- }, 100);
- };
- const features = [
- { key: 'deep-think', label: '深度思考/思维链', icon: '🔬' },
- { key: 'search', label: '联网搜索', icon: '🌐' },
- ];
- const suggestions = [
- '专业知识',
- '职能管理',
- '项目级应用',
- ];
- return (
- <div className="chat-main">
- {/* Back Button */}
- <button className="back-btn" onClick={handleBack}>
- <ArrowLeftOutlined /> 开放平台
- </button>
- {/* Chat Messages */}
- <div className="chat-messages">
- {messages.length === 0 ? (
- <WelcomeScreen
- features={features}
- activeFeatures={activeFeatures}
- toggleFeature={toggleFeature}
- suggestions={suggestions}
- onSuggestionClick={handleSuggestionClick}
- inputValue={inputValue}
- setInputValue={setInputValue}
- onSend={handleSendMessage}
- loading={loading}
- inputRef={inputRef}
- />
- ) : (
- <>
- {messages.map((msg) => (
- <div key={msg.id} className={`message-bubble ${msg.role}`}>
- <div className="message-avatar">
- {msg.role === 'assistant' ? '🤖' : '👤'}
- </div>
- <div className="bubble-content">
- <ReactMarkdown>{msg.content}</ReactMarkdown>
- {msg.role === 'assistant' && (
- <div style={{ display: 'flex', gap: '8px', marginTop: '8px', opacity: 0.6 }}>
- <button onClick={() => handleCopy(msg.content)} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
- <CopyOutlined style={{ fontSize: '14px' }} />
- </button>
- <button style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
- <LikeOutlined style={{ fontSize: '14px' }} />
- </button>
- <button style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
- <DislikeOutlined style={{ fontSize: '14px' }} />
- </button>
- </div>
- )}
- </div>
- </div>
- ))}
- {loading && (
- <div className="message-bubble assistant">
- <div className="message-avatar">🤖</div>
- <div className="bubble-content">
- <div className="loading-indicator">
- <div className="loading-dot"></div>
- <div className="loading-dot"></div>
- <div className="loading-dot"></div>
- </div>
- </div>
- </div>
- )}
- <div ref={messagesEndRef} />
- </>
- )}
- </div>
- {/* Footer */}
- <div className="chat-footer">
- <div className="footer-content">
- <span>内容由 AI 生成,请仔细甄别</span>
- <span className="footer-divider">|</span>
- <a className="footer-link" href="https://beian.miit.gov.cn/#/Integrated/index" target="_blank" rel="noopener noreferrer">
- 上海建科工程咨询有限公司
- </a>
- </div>
- </div>
- </div>
- );
- };
- // Welcome Screen Component
- interface WelcomeScreenProps {
- features: Array<{ key: string; label: string; icon: string; isNew?: boolean }>;
- activeFeatures: Set<string>;
- toggleFeature: (feature: string) => void;
- suggestions: string[];
- onSuggestionClick: (text: string) => void;
- inputValue: string;
- setInputValue: (value: string) => void;
- onSend: () => void;
- loading: boolean;
- inputRef: React.RefObject<HTMLDivElement>;
- }
- const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
- features,
- activeFeatures,
- toggleFeature,
- suggestions,
- onSuggestionClick,
- inputValue,
- setInputValue,
- onSend,
- loading,
- inputRef,
- }) => {
- return (
- <div className="welcome-screen">
- {/* Logo */}
- <div className="welcome-logo">
- <img src={aiIcon} alt="AI Icon" className="logo-icon" />
- <div className="logo-text">建科小智</div>
- <div className="logo-slogan">,智能问答</div>
- </div>
- {/* Input Container */}
- <div className="welcome-input-container">
- <div className="chat-input-wrapper">
- <div
- ref={inputRef}
- className="input-placeholder"
- contentEditable
- onInput={(e) => setInputValue(e.currentTarget.textContent || '')}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- onSend();
- }
- }}
- suppressContentEditableWarning
- data-placeholder="请输入你的问题或需求"
- />
- {/* Input Actions - 包含功能按钮和发送按钮 */}
- <div className="input-actions">
- <div className="left-actions">
- {/* 附件按钮 */}
- <button className="action-btn" title="上传附件">
- <PaperClipOutlined />
- </button>
-
- {/* 功能按钮 */}
- {features.map((feature) => (
- <button
- key={feature.key}
- className={`feature-btn ${activeFeatures.has(feature.key) ? 'active' : ''}`}
- onClick={() => toggleFeature(feature.key)}
- title={feature.label}
- >
- <span className="feature-icon">{feature.icon}</span>
- <span className="feature-label">{feature.label}</span>
- {feature.isNew && <span className="new-tag">new</span>}
- </button>
- ))}
- </div>
- <div className="right-actions">
- <button className="voice-btn" title="语音输入">
- <PhoneOutlined />
- </button>
- <button
- className="send-btn"
- onClick={onSend}
- disabled={!inputValue.trim() || loading}
- title="发送"
- >
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
- </svg>
- </button>
- </div>
- </div>
- </div>
- </div>
- {/* Suggestion Tags */}
- <div className="suggestion-container">
- <div className="suggestion-title">我猜你在找...</div>
- <div className="suggestion-list">
- {suggestions.map((text, index) => (
- <div key={index} className="suggestion-item" onClick={() => onSuggestionClick(text)}>
- {text}
- </div>
- ))}
- </div>
- </div>
- </div>
- );
- };
|