基于 shiy-temp分支修改

This commit is contained in:
pingchuan
2025-06-04 11:18:52 +08:00
parent 87ad3376fe
commit af33de09db
36 changed files with 6293 additions and 105 deletions
+256
View File
@@ -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>
);
}