feat(ChatInput): 集成快捷短语功能

- 添加快捷短语触发按钮和弹出层
- 实现短语内容插入到输入框功能
- 集成快捷短语管理模态框
- 添加点击外部和ESC键关闭弹出层
- 统一按钮圆角样式为 rounded-md
This commit is contained in:
gaoziman 2025-12-24 00:08:17 +08:00
parent caf19f4c09
commit 16079af79d

View File

@ -5,11 +5,14 @@ import { Paperclip, ArrowUp, Upload } from 'lucide-react';
import { ModelSelector } from './ModelSelector';
import { ToolsDropdown } from './ToolsDropdown';
import { FilePreviewList } from './FilePreviewList';
import { QuickPhrasesTrigger, QuickPhrasesPopover } from './QuickPhrasesPopover';
import { QuickPhrasesModal } from './QuickPhrasesModal';
import { Tooltip } from '@/components/ui/Tooltip';
import { useFileUpload } from '@/hooks/useFileUpload';
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
import { useQuickPhrases } from '@/hooks/useQuickPhrases';
import { cn } from '@/lib/utils';
import type { Model, Tool } from '@/types';
import type { Model, Tool, QuickPhrase } from '@/types';
import type { UploadFile } from '@/types/file-upload';
interface ChatInputProps {
@ -36,11 +39,22 @@ export function ChatInput({
className,
}: ChatInputProps) {
const [message, setMessage] = useState('');
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
const [isPhrasesOpen, setIsPhrasesOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const phrasesContainerRef = useRef<HTMLDivElement>(null);
// 使用提示词优化 Hook
const { consumeOptimizedPrompt } = usePromptOptimizer();
// 使用快捷短语 Hook
const {
phrases,
addPhrase,
updatePhrase,
deletePhrase,
} = useQuickPhrases();
// 监听优化后的提示词并填入输入框
useEffect(() => {
const prompt = consumeOptimizedPrompt();
@ -49,6 +63,39 @@ export function ChatInput({
}
}, [consumeOptimizedPrompt]);
// 点击外部关闭快捷短语弹出层
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
phrasesContainerRef.current &&
!phrasesContainerRef.current.contains(event.target as Node)
) {
setIsPhrasesOpen(false);
}
}
if (isPhrasesOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isPhrasesOpen]);
// ESC键关闭快捷短语弹出层
useEffect(() => {
function handleEscKey(event: KeyboardEvent) {
if (event.key === 'Escape' && isPhrasesOpen) {
setIsPhrasesOpen(false);
}
}
if (isPhrasesOpen) {
document.addEventListener('keydown', handleEscKey);
}
return () => document.removeEventListener('keydown', handleEscKey);
}, [isPhrasesOpen]);
// 使用文件上传 Hook
const {
files,
@ -97,9 +144,36 @@ export function ChatInput({
fileInputRef.current?.click();
};
// 插入快捷短语内容
const handleInsertPhrase = (content: string) => {
setMessage((prev) => {
// 如果已有内容,在末尾添加空格
if (prev.trim()) {
return prev + ' ' + content;
}
return content;
});
};
// 打开管理模态框
const handleOpenManageModal = () => {
setIsManageModalOpen(true);
};
// 关闭管理模态框
const handleCloseManageModal = () => {
setIsManageModalOpen(false);
};
// 处理添加短语(从管理模态框)
const handleAddFromModal = () => {
handleOpenManageModal();
};
return (
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
<div
ref={phrasesContainerRef}
className={cn(
'relative flex flex-col bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md p-4 shadow-[var(--shadow-input)]',
'transition-all duration-150',
@ -111,6 +185,19 @@ export function ChatInput({
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* 快捷短语弹出层 - 横跨在输入框顶部 */}
<QuickPhrasesPopover
phrases={phrases}
isOpen={isPhrasesOpen}
onInsert={handleInsertPhrase}
onEdit={(phrase) => {
setIsManageModalOpen(true);
}}
onDelete={deletePhrase}
onManage={handleOpenManageModal}
onAdd={handleAddFromModal}
onClose={() => setIsPhrasesOpen(false)}
/>
{/* 拖拽覆盖层 */}
{isDragging && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-[var(--color-bg-primary)]/90 rounded-md border-2 border-dashed border-[var(--color-primary)]">
@ -153,7 +240,7 @@ export function ChatInput({
<button
onClick={openFileDialog}
className={cn(
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors cursor-pointer',
'w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer',
'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]',
files.length > 0 && 'text-[var(--color-primary)]'
)}
@ -172,6 +259,12 @@ export function ChatInput({
className="hidden"
/>
{/* 快捷短语触发按钮 */}
<QuickPhrasesTrigger
phrasesCount={phrases.length}
onClick={() => setIsPhrasesOpen(!isPhrasesOpen)}
/>
{/* 工具下拉 */}
<ToolsDropdown
tools={tools}
@ -195,7 +288,7 @@ export function ChatInput({
onClick={handleSend}
disabled={!message.trim() && files.length === 0}
className={cn(
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-xl transition-all duration-150 cursor-pointer',
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-md transition-all duration-150 cursor-pointer',
message.trim() || files.length > 0
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
: 'opacity-50 cursor-not-allowed'
@ -212,6 +305,16 @@ export function ChatInput({
<p className="text-xs text-[var(--color-text-tertiary)] text-center mt-2">
使 Ctrl+V
</p>
{/* 快捷短语管理模态框 */}
<QuickPhrasesModal
isOpen={isManageModalOpen}
onClose={handleCloseManageModal}
phrases={phrases}
onAdd={addPhrase}
onUpdate={updatePhrase}
onDelete={deletePhrase}
/>
</div>
);
}