import { ArrowUpOutlined, BookOutlined, CloseOutlined, DownOutlined, PaperClipOutlined } from '@ant-design/icons'; import { Button, Image as AntImage, Input, Select, Tag, Tooltip, Upload, Popover } from 'antd'; import type { ChatAttachment } from '../../../api'; import type { Agent } from '../../../api/agents'; import { HistoryIcon, NewChatIcon } from '../../../components/icons'; import { useState, useRef, useEffect } from 'react'; export default function ChatInput(props: { input: string; setInput: (v: string) => void; sending: boolean; attachments: ChatAttachment[]; setAttachments: (updater: (prev: ChatAttachment[]) => ChatAttachment[]) => ChatAttachment[]; imageUrls: string[]; setImageUrls: (updater: (prev: string[]) => string[]) => void; onSend: () => void; onStop: () => void; onAttach: (files: File[]) => void; onOpenTpl: () => void; modelOptions: Array<{ value: string; label: string }>; activeModelValue: string; onChangeModel: (modelId: string) => void; onOpenHistory: () => void; onNewSession: () => void; agentList: Agent[]; onInsertMention: (agentName: string) => void; }) { const { input, setInput, sending, attachments, setAttachments, imageUrls, setImageUrls, onSend, onStop, onAttach, onOpenTpl, modelOptions, activeModelValue, onChangeModel, onOpenHistory, onNewSession, agentList, onInsertMention } = props; const [showMentionPopover, setShowMentionPopover] = useState(false); const [mentionQuery, setMentionQuery] = useState(''); const [mentionPos, setMentionPos] = useState<{ top: number; left: number } | null>(null); const inputRef = useRef(null); // 检测 @ 触发提及选择 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setInput(value); const cursorPos = e.target.selectionStart ?? value.length; const textBeforeCursor = value.slice(0, cursorPos); const atIndex = textBeforeCursor.lastIndexOf('@'); if (atIndex !== -1 && (atIndex === 0 || /\s$/.test(textBeforeCursor.slice(0, atIndex)))) { const query = textBeforeCursor.slice(atIndex + 1); if (!query.includes(' ')) { setMentionQuery(query); // 计算位置 const textarea = e.target; const rect = textarea.getBoundingClientRect(); setMentionPos({ top: rect.bottom, left: rect.left + 10 }); setShowMentionPopover(true); return; } } setShowMentionPopover(false); }; const filteredAgents = agentList.filter(a => a.name.toLowerCase().includes(mentionQuery.toLowerCase()) ); const handleSelectAgent = (agent: Agent) => { const textarea = inputRef.current; if (!textarea) return; const value = input; const cursorPos = textarea.selectionStart ?? value.length; const textBefore = value.slice(0, cursorPos); const atIndex = textBefore.lastIndexOf('@'); if (atIndex === -1) { setShowMentionPopover(false); return; } const newValue = value.slice(0, atIndex) + `@${agent.name} ` + value.slice(cursorPos); setInput(newValue); setShowMentionPopover(false); requestAnimationFrame(() => { textarea.focus(); const newCursor = atIndex + agent.name.length + 2; textarea.setSelectionRange(newCursor, newCursor); }); }; return (
{attachments.map((a, i) => ( setAttachments((arr) => arr.filter((_, j) => j !== i))}> 📎 {a.name} ))} {imageUrls.map((u, i) => (
))}
{ if (e.key !== 'Enter') return; if ((e as any).isComposing) return; if (e.metaKey || e.ctrlKey) { e.preventDefault(); const el = e.currentTarget; const start = el.selectionStart ?? input.length; const end = el.selectionEnd ?? input.length; const next = input.slice(0, start) + '\n' + input.slice(end); setInput(next); requestAnimationFrame(() => { el.selectionStart = el.selectionEnd = start + 1; }); return; } if (!e.shiftKey && !e.altKey) { e.preventDefault(); onSend(); } }} className="chat-input-textarea" disabled={sending} /> {showMentionPopover && mentionPos && (
{filteredAgents.length === 0 ? (
未找到匹配的智能体
) : ( filteredAgents.map(agent => (
handleSelectAgent(agent)} style={{ padding: '6px 10px', cursor: 'pointer', borderBottom: '1px solid var(--color-border)' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--color-fill-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'} >
{agent.name}
{agent.description && (
{agent.description.slice(0, 30)} {agent.description.length > 30 ? '...' : ''}
)}
)) )}
)}