/** * AIChatComponent - AI 对话客户端组件 (完整版) * * 功能特性: * - 支持多种对话模式 (助手/查询/创作/工作流指导) * - 智能建议展示 * - 输入状态指示器 * - 附件上传支持 * - Markdown 渲染 * - 代码高亮显示 * - 自动滚动到底部 * - 上下文记忆 * * @example * const chat = new AIChatComponent({ * containerId: 'chat-container', * mode: 'assistant', * apiEndpoint: '/api/chat', * enableSuggestions: true, * placeholder: '请输入您的问题...' * }); * chat.initialize(); * chat.sendMessage('如何创建工作流?'); */ class AIChatComponent { constructor(options = {}) { // 配置项 this.containerId = options.containerId || 'ai-chat-container'; this.mode = options.mode || 'assistant'; // assistant/query/creative/workflow_guidance this.apiEndpoint = options.apiEndpoint || '/api/chat/completions'; this.apiBaseUrl = options.apiBaseUrl || ''; this.enableSuggestions = options.enableSuggestions !== false; this.enableAttachments = options.enableAttachments || false; this.enableMarkdown = options.enableMarkdown || true; this.placeholder = options.placeholder || '请输入...'; this.suggestions = options.suggestions || []; this.contextWindow = options.contextWindow || 10; // 保留最近 10 轮对话 this.onMessageSent = options.onMessageSent || null; // 状态 this.messages = []; this.isLoading = false; this.conversationId = null; // DOM 元素 this.container = null; this.messagesContainer = null; this.inputField = null; this.sendButton = null; this.attachmentsInput = null; // 样式配置 this.styles = { primaryColor: '#667eea', secondaryColor: '#764ba2', userBubbleBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', aiBubbleBg: '#f0f0f0' }; } /** * 初始化组件 */ initialize() { this.container = document.getElementById(this.containerId); if (!this.container) { console.error(`AIChat: 容器 #${this.containerId} 不存在`); return false; } this.render(); this.bindEvents(); this.loadSuggestions(); // 生成会话 ID this.conversationId = `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return true; } /** * 渲染组件 HTML 结构 */ render() { this.container.innerHTML = `
🤖
AI 智能助手
${this.getModeDescription()}
${this.enableSuggestions ? ` ` : ''}
${this.enableAttachments ? `
` : ''}
`; // 获取 DOM 引用 this.messagesContainer = document.getElementById(`${this.containerId}-messages`); this.inputField = document.getElementById(`${this.containerId}-input`); this.sendButton = document.getElementById(`${this.containerId}-send`); if (this.enableAttachments) { this.attachmentsInput = document.getElementById(`${this.containerId}-attachments`); } } /** * 绑定事件监听器 */ bindEvents() { // 发送按钮点击 this.sendButton.addEventListener('click', () => this.sendMessage()); // 回车发送 (Shift+Enter 换行) this.inputField.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // 自动调整 textarea 高度 this.inputField.addEventListener('input', () => { this.inputField.style.height = 'auto'; const newHeight = Math.min(this.inputField.scrollHeight, 120); this.inputField.style.height = `${newHeight}px`; }); // 附件变化 if (this.attachmentsInput) { this.attachmentsInput.addEventListener('change', (e) => { const count = e.target.files.length; const counter = document.getElementById(`${this.containerId}-attachments-count`); if (counter) { counter.textContent = count > 0 ? `已选择 ${count} 个文件` : ''; } }); } } /** * 加载智能建议 */ async loadSuggestions() { if (!this.enableSuggestions) return; // 根据模式加载不同的建议 const suggestionMap = { 'assistant': ['如何使用这个系统?', '有哪些核心功能?', '怎样快速上手?'], 'query': ['查询我的数据', '查看统计信息', '导出报表'], 'creative': ['帮我写一篇文章', '生成营销文案', '创作短视频脚本'], 'workflow_guidance': ['如何创建工作流?', '怎样配置节点?', '执行失败怎么办?'] }; let suggestions = this.suggestions.length > 0 ? this.suggestions : (suggestionMap[this.mode] || []); if (suggestions.length === 0) return; const suggestionsContainer = document.getElementById(`${this.containerId}-suggestions`); const suggestionsList = suggestionsContainer.querySelector('div'); suggestions.forEach(text => { const chip = document.createElement('button'); chip.textContent = text; chip.style.cssText = ` background: #f0f0f0; border: none; padding: 6px 12px; border-radius: 16px; font-size: 13px; color: #666; cursor: pointer; transition: all 0.2s; `; chip.addEventListener('mouseenter', () => { chip.style.background = this.styles.primaryColor; chip.style.color = 'white'; }); chip.addEventListener('mouseleave', () => { chip.style.background = '#f0f0f0'; chip.style.color = '#666'; }); chip.addEventListener('click', () => { this.inputField.value = text; this.sendMessage(); }); suggestionsList.appendChild(chip); }); suggestionsContainer.style.display = 'block'; } /** * 添加消息到聊天历史 */ appendMessage(role, content, options = {}) { const message = { id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, role: role, // 'user' or 'ai' content: content, timestamp: new Date().toISOString(), attachments: options.attachments || [], metadata: options.metadata || {} }; this.messages.push(message); // 限制上下文窗口大小 if (this.messages.length > this.contextWindow) { this.messages = this.messages.slice(-this.contextWindow); } this.renderMessage(message); this.scrollToBottom(); return message; } /** * 渲染单条消息 */ renderMessage(message) { const messageEl = document.createElement('div'); messageEl.className = `ai-chat-message ${message.role}-message`; messageEl.style.cssText = ` margin-bottom: 20px; display: flex; ${message.role === 'user' ? 'justify-content: flex-end;' : 'justify-content: flex-start;'} `; const bubble = document.createElement('div'); bubble.style.cssText = ` max-width: 70%; padding: 12px 16px; border-radius: 12px; background: ${message.role === 'user' ? this.styles.userBubbleBg : this.styles.aiBubbleBg}; color: ${message.role === 'user' ? 'white' : '#333'}; word-wrap: break-word; box-shadow: 0 2px 8px rgba(0,0,0,0.1); `; // 消息内容 let contentHtml = message.content; if (this.enableMarkdown && message.role === 'ai') { // TODO: 集成 Markdown 渲染 contentHtml = this.simpleMarkdownRender(message.content); } bubble.innerHTML = `
${contentHtml}
${message.attachments.length > 0 ? `
${message.attachments.map(att => `
📎 ${att.name}
`).join('')}
` : ''}
${new Date(message.timestamp).toLocaleTimeString()}
`; messageEl.appendChild(bubble); this.messagesContainer.appendChild(messageEl); } /** * 简单的 Markdown 渲染 (简化版) */ simpleMarkdownRender(text) { // 代码块 text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { return `
${code.trim()}
`; }); // 行内代码 text = text.replace(/`([^`]+)`/g, '$1'); // 粗体 text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // 斜体 text = text.replace(/\*([^*]+)\*/g, '$1'); // 链接 text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // 换行 text = text.replace(/\n/g, '
'); return text; } /** * 发送消息 */ async sendMessage(customMessage = null) { const message = customMessage || this.inputField.value.trim(); if (!message && !customMessage) return; // 添加用户消息 this.appendMessage('user', message, { attachments: this.getAttachments() }); // 清空输入框 if (!customMessage) { this.inputField.value = ''; this.inputField.style.height = 'auto'; } // 回调 if (this.onMessageSent) { this.onMessageSent(message); } // 调用 API await this.callAIAPI(message); } /** * 获取附件列表 */ getAttachments() { if (!this.attachmentsInput) return []; const files = Array.from(this.attachmentsInput.files); return files.map(file => ({ name: file.name, size: file.size, type: file.type })); } /** * 调用后端 AI API */ async callAIAPI(message) { this.setLoading(true); try { const response = await fetch(`${this.apiBaseUrl}${this.apiEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}` }, body: JSON.stringify({ conversation_id: this.conversationId, message: message, mode: this.mode, context: this.messages.slice(-5).map(m => ({ role: m.role, content: m.content })) }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); // 添加 AI 回复 const aiResponse = result.reply || result.message || result.content || '抱歉,我暂时无法回答这个问题。'; this.appendMessage('ai', aiResponse); } catch (error) { console.error('AI API 调用失败:', error); // 显示错误提示 this.appendMessage('ai', `❌ 调用失败:${error.message}\n\n请稍后重试,或联系管理员。`, { metadata: { error: true } }); } finally { this.setLoading(false); } } /** * 设置加载状态 */ setLoading(loading) { this.isLoading = loading; if (loading) { this.sendButton.disabled = true; this.sendButton.innerHTML = ' 思考中...'; this.sendButton.style.opacity = '0.7'; } else { this.sendButton.disabled = false; this.sendButton.innerHTML = '📤 发送'; this.sendButton.style.opacity = '1'; } } /** * 滚动到底部 */ scrollToBottom() { setTimeout(() => { this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; }, 50); } /** * 获取模式描述 */ getModeDescription() { const modeDescriptions = { 'assistant': '您的全能 AI 助手', 'query': '数据查询与分析专家', 'creative': '创意内容生成专家', 'workflow_guidance': '工作流编排指导专家' }; return modeDescriptions[this.mode] || 'AI 智能助手'; } /** * 清空聊天记录 */ clearMessages() { this.messages = []; this.messagesContainer.innerHTML = ''; } /** * 导出聊天记录 */ exportHistory() { const blob = new Blob([JSON.stringify(this.messages, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chat-history-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } } // 导出到全局 if (typeof window !== 'undefined') { window.AIChatComponent = AIChatComponent; }