feat(ChatInput): 集成快捷短语功能
- 添加快捷短语触发按钮和弹出层 - 实现短语内容插入到输入框功能 - 集成快捷短语管理模态框 - 添加点击外部和ESC键关闭弹出层 - 统一按钮圆角样式为 rounded-md
This commit is contained in:
parent
caf19f4c09
commit
16079af79d
@ -5,11 +5,14 @@ import { Paperclip, ArrowUp, Upload } from 'lucide-react';
|
|||||||
import { ModelSelector } from './ModelSelector';
|
import { ModelSelector } from './ModelSelector';
|
||||||
import { ToolsDropdown } from './ToolsDropdown';
|
import { ToolsDropdown } from './ToolsDropdown';
|
||||||
import { FilePreviewList } from './FilePreviewList';
|
import { FilePreviewList } from './FilePreviewList';
|
||||||
|
import { QuickPhrasesTrigger, QuickPhrasesPopover } from './QuickPhrasesPopover';
|
||||||
|
import { QuickPhrasesModal } from './QuickPhrasesModal';
|
||||||
import { Tooltip } from '@/components/ui/Tooltip';
|
import { Tooltip } from '@/components/ui/Tooltip';
|
||||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||||
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
|
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
|
||||||
|
import { useQuickPhrases } from '@/hooks/useQuickPhrases';
|
||||||
import { cn } from '@/lib/utils';
|
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';
|
import type { UploadFile } from '@/types/file-upload';
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
@ -36,11 +39,22 @@ export function ChatInput({
|
|||||||
className,
|
className,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||||
|
const [isPhrasesOpen, setIsPhrasesOpen] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const phrasesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 使用提示词优化 Hook
|
// 使用提示词优化 Hook
|
||||||
const { consumeOptimizedPrompt } = usePromptOptimizer();
|
const { consumeOptimizedPrompt } = usePromptOptimizer();
|
||||||
|
|
||||||
|
// 使用快捷短语 Hook
|
||||||
|
const {
|
||||||
|
phrases,
|
||||||
|
addPhrase,
|
||||||
|
updatePhrase,
|
||||||
|
deletePhrase,
|
||||||
|
} = useQuickPhrases();
|
||||||
|
|
||||||
// 监听优化后的提示词并填入输入框
|
// 监听优化后的提示词并填入输入框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prompt = consumeOptimizedPrompt();
|
const prompt = consumeOptimizedPrompt();
|
||||||
@ -49,6 +63,39 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
}, [consumeOptimizedPrompt]);
|
}, [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
|
// 使用文件上传 Hook
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
@ -97,9 +144,36 @@ export function ChatInput({
|
|||||||
fileInputRef.current?.click();
|
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 (
|
return (
|
||||||
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
|
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
|
||||||
<div
|
<div
|
||||||
|
ref={phrasesContainerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex flex-col bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md p-4 shadow-[var(--shadow-input)]',
|
'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',
|
'transition-all duration-150',
|
||||||
@ -111,6 +185,19 @@ export function ChatInput({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
|
{/* 快捷短语弹出层 - 横跨在输入框顶部 */}
|
||||||
|
<QuickPhrasesPopover
|
||||||
|
phrases={phrases}
|
||||||
|
isOpen={isPhrasesOpen}
|
||||||
|
onInsert={handleInsertPhrase}
|
||||||
|
onEdit={(phrase) => {
|
||||||
|
setIsManageModalOpen(true);
|
||||||
|
}}
|
||||||
|
onDelete={deletePhrase}
|
||||||
|
onManage={handleOpenManageModal}
|
||||||
|
onAdd={handleAddFromModal}
|
||||||
|
onClose={() => setIsPhrasesOpen(false)}
|
||||||
|
/>
|
||||||
{/* 拖拽覆盖层 */}
|
{/* 拖拽覆盖层 */}
|
||||||
{isDragging && (
|
{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)]">
|
<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
|
<button
|
||||||
onClick={openFileDialog}
|
onClick={openFileDialog}
|
||||||
className={cn(
|
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)]',
|
'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]',
|
||||||
files.length > 0 && 'text-[var(--color-primary)]'
|
files.length > 0 && 'text-[var(--color-primary)]'
|
||||||
)}
|
)}
|
||||||
@ -172,6 +259,12 @@ export function ChatInput({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 快捷短语触发按钮 */}
|
||||||
|
<QuickPhrasesTrigger
|
||||||
|
phrasesCount={phrases.length}
|
||||||
|
onClick={() => setIsPhrasesOpen(!isPhrasesOpen)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 工具下拉 */}
|
{/* 工具下拉 */}
|
||||||
<ToolsDropdown
|
<ToolsDropdown
|
||||||
tools={tools}
|
tools={tools}
|
||||||
@ -195,7 +288,7 @@ export function ChatInput({
|
|||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!message.trim() && files.length === 0}
|
disabled={!message.trim() && files.length === 0}
|
||||||
className={cn(
|
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
|
message.trim() || files.length > 0
|
||||||
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
|
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
|
||||||
: 'opacity-50 cursor-not-allowed'
|
: '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">
|
<p className="text-xs text-[var(--color-text-tertiary)] text-center mt-2">
|
||||||
可拖拽文件到此处或使用 Ctrl+V 粘贴图片
|
可拖拽文件到此处或使用 Ctrl+V 粘贴图片
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* 快捷短语管理模态框 */}
|
||||||
|
<QuickPhrasesModal
|
||||||
|
isOpen={isManageModalOpen}
|
||||||
|
onClose={handleCloseManageModal}
|
||||||
|
phrases={phrases}
|
||||||
|
onAdd={addPhrase}
|
||||||
|
onUpdate={updatePhrase}
|
||||||
|
onDelete={deletePhrase}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user