基于 shiy-temp分支修改
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd';
|
||||
import { SendOutlined, StopOutlined, PaperClipOutlined, PictureOutlined } from '@ant-design/icons';
|
||||
import type { VisionFile } from '../../types/dify_chat';
|
||||
import '../../styles/components/chat-with-llm/chat-input.css';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface ChatInputProps {
|
||||
onSendMessage: (message: string, files?: VisionFile[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
onStop?: () => void;
|
||||
isResponding?: boolean;
|
||||
visionConfig?: {
|
||||
enabled: boolean;
|
||||
number_limits?: number;
|
||||
image_file_size_limit?: number;
|
||||
transfer_methods?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天输入组件
|
||||
*/
|
||||
export default function ChatInput({
|
||||
onSendMessage,
|
||||
disabled = false,
|
||||
placeholder = '输入消息...',
|
||||
onStop,
|
||||
isResponding = false,
|
||||
visionConfig,
|
||||
}: ChatInputProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [files, setFiles] = useState<VisionFile[]>([]);
|
||||
const textareaRef = useRef<any>(null);
|
||||
const isComposing = useRef(false);
|
||||
|
||||
/**
|
||||
* 提交消息
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
if (!message.trim() || disabled) return;
|
||||
|
||||
onSendMessage(message, files.length > 0 ? files : undefined);
|
||||
setMessage('');
|
||||
setFiles([]);
|
||||
|
||||
// 聚焦回输入框
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理键盘事件
|
||||
*/
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// 处理输入法状态
|
||||
if (e.nativeEvent.isComposing) {
|
||||
isComposing.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter发送,Shift+Enter换行
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing.current) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理输入法结束
|
||||
*/
|
||||
const handleCompositionEnd = () => {
|
||||
isComposing.current = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止响应
|
||||
*/
|
||||
const handleStop = () => {
|
||||
onStop?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件上传
|
||||
*/
|
||||
const handleFileUpload = (file: File) => {
|
||||
// 检查文件数量限制
|
||||
if (visionConfig?.number_limits && files.length >= visionConfig.number_limits) {
|
||||
antdMessage.error(`最多只能上传 ${visionConfig.number_limits} 个文件`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小限制
|
||||
if (visionConfig?.image_file_size_limit) {
|
||||
const limitMB = visionConfig.image_file_size_limit;
|
||||
const fileSizeMB = file.size / (1024 * 1024);
|
||||
if (fileSizeMB > limitMB) {
|
||||
antdMessage.error(`文件大小不能超过 ${limitMB}MB`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
antdMessage.error('只支持图片文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建文件对象
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const newFile: VisionFile = {
|
||||
id: `file-${Date.now()}-${Math.random()}`,
|
||||
type: 'image',
|
||||
transfer_method: 'local_file' as any,
|
||||
url: e.target?.result as string,
|
||||
upload_file_id: '',
|
||||
};
|
||||
|
||||
setFiles(prev => [...prev, newFile]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除文件
|
||||
*/
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
setFiles(prev => prev.filter(file => file.id !== fileId));
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染文件预览
|
||||
*/
|
||||
const renderFilePreview = () => {
|
||||
if (files.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mb-2 p-2 bg-gray-50 rounded">
|
||||
{files.map((file) => (
|
||||
<div key={file.id} className="relative group">
|
||||
<img
|
||||
src={file.url}
|
||||
alt="预览"
|
||||
className="w-16 h-16 object-cover rounded border"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => handleRemoveFile(file.id!)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染上传按钮
|
||||
*/
|
||||
const renderUploadButton = () => {
|
||||
if (!visionConfig?.enabled) return null;
|
||||
|
||||
const isDisabled = disabled || (visionConfig.number_limits ? files.length >= visionConfig.number_limits : false);
|
||||
|
||||
return (
|
||||
<Upload
|
||||
beforeUpload={handleFileUpload}
|
||||
showUploadList={false}
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
>
|
||||
<Tooltip title="上传图片">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PictureOutlined />}
|
||||
size="small"
|
||||
disabled={isDisabled}
|
||||
className="text-gray-500 hover:text-blue-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-200 bg-white p-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* 文件预览 */}
|
||||
{renderFilePreview()}
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="relative">
|
||||
<div className="flex items-end gap-2">
|
||||
{/* 上传按钮 */}
|
||||
<div className="flex items-end pb-1">
|
||||
{renderUploadButton()}
|
||||
</div>
|
||||
|
||||
{/* 文本输入 */}
|
||||
<div className="flex-1">
|
||||
<TextArea
|
||||
ref={textareaRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
autoSize={{ minRows: 1, maxRows: 6 }}
|
||||
className="resize-none"
|
||||
style={{ paddingRight: '50px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 发送/停止按钮 */}
|
||||
<div className="flex items-end pb-1">
|
||||
{isResponding ? (
|
||||
<Tooltip title="停止生成">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={handleStop}
|
||||
disabled={disabled}
|
||||
size="large"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="发送消息 (Enter)">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSubmit}
|
||||
disabled={disabled || !message.trim()}
|
||||
size="large"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 字符计数和提示 */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user