131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
import type { KeyboardEvent, ReactNode } from 'react';
|
|
import { Send, Square, Upload } from 'lucide-react';
|
|
import { Textarea } from 'tdesign-react';
|
|
import { useRef } from 'react';
|
|
|
|
interface ChatInputComposerProps {
|
|
value: string;
|
|
onChange?: (value: string) => void;
|
|
onSend?: () => void;
|
|
onStop?: () => void;
|
|
onKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
isSending?: boolean;
|
|
isDarkMode: boolean;
|
|
trailingActions?: ReactNode;
|
|
readOnly?: boolean;
|
|
className?: string;
|
|
showBorderTop?: boolean;
|
|
showUpload?: boolean;
|
|
onUploadFile?: (file: File) => void | Promise<void>;
|
|
}
|
|
|
|
export default function ChatInputComposer({
|
|
value,
|
|
onChange,
|
|
onSend,
|
|
onStop,
|
|
onKeyDown,
|
|
placeholder = '输入你的问题...',
|
|
disabled = false,
|
|
isSending = false,
|
|
isDarkMode,
|
|
trailingActions,
|
|
readOnly = false,
|
|
className = '',
|
|
showBorderTop = false,
|
|
showUpload = true,
|
|
onUploadFile,
|
|
}: ChatInputComposerProps) {
|
|
const submitEnabled = Boolean(value.trim()) && !disabled && !readOnly && Boolean(onSend);
|
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
const iconButtonClass = `rounded-lg p-2 transition-colors ${
|
|
isDarkMode ? 'text-slate-400 hover:bg-slate-600 hover:text-slate-200' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
|
|
}`;
|
|
|
|
const handleKeyDown = (val, context) => {
|
|
if (context?.e?.key === 'Enter') {
|
|
// 阻止默认的换行行为(如果是 Textarea)
|
|
context.e.preventDefault();
|
|
|
|
// 模拟发送请求
|
|
onSend()
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`${showBorderTop ? (isDarkMode ? 'border-t border-slate-700/50' : 'border-t border-red-100') : ''} p-4 ${className}`}>
|
|
<div className={`flex items-end gap-3 rounded-2xl p-3 shadow-inner ${isDarkMode ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
|
<Textarea
|
|
value={value}
|
|
onChange={(nextValue) => onChange?.(String(nextValue))}
|
|
onKeydown={handleKeyDown}
|
|
placeholder={placeholder}
|
|
autosize={{ minRows: 1, maxRows: 8 }}
|
|
disabled={disabled}
|
|
readonly={readOnly}
|
|
className={`flex-1 !bg-transparent !border-0 !shadow-none qa-composer-textarea [&]:!border-0 [&]:!shadow-none [&]:!outline-none [&]:focus:!border-0 [&]:focus:!shadow-none [&]:focus:!outline-none [&_.t-textarea]:!border-0 [&_.t-textarea]:!shadow-none [&_.t-textarea]:!outline-none [&_.t-textarea:hover]:!border-0 [&_.t-textarea:hover]:!shadow-none [&_.t-textarea--focused]:!border-0 [&_.t-textarea--focused]:!shadow-none [&_.t-is-focused]:!border-0 [&_.t-is-focused]:!shadow-none [&_.t-textarea__inner]:!min-h-[40px] [&_.t-textarea__inner]:!resize-none [&_.t-textarea__inner]:!bg-transparent [&_.t-textarea__inner]:!border-0 [&_.t-textarea__inner]:!p-0 [&_.t-textarea__inner]:!shadow-none [&_.t-textarea__inner]:!outline-none [&_.t-textarea__inner:hover]:!border-0 [&_.t-textarea__inner:hover]:!shadow-none [&_.t-textarea__inner:focus]:!border-0 [&_.t-textarea__inner:focus]:!shadow-none [&_.t-textarea__inner:focus]:!outline-none [&_.t-textarea__inner]:disabled:!cursor-not-allowed ${
|
|
isDarkMode
|
|
? '[&_.t-textarea__inner]:!text-white [&_.t-textarea__inner]:placeholder:!text-slate-500'
|
|
: '[&_.t-textarea__inner]:!text-gray-800 [&_.t-textarea__inner]:placeholder:!text-gray-400'
|
|
}`}
|
|
/>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{showUpload && (
|
|
<>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
className="hidden"
|
|
onChange={(event) => {
|
|
const file = event.target.files?.[0];
|
|
if (file) {
|
|
void onUploadFile?.(file);
|
|
}
|
|
event.target.value = '';
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => inputRef.current?.click()}
|
|
disabled={disabled || !onUploadFile}
|
|
className={`${iconButtonClass} ${(disabled || !onUploadFile) ? 'cursor-not-allowed opacity-50 hover:bg-transparent hover:text-inherit' : ''}`}
|
|
title="上传文件"
|
|
>
|
|
<Upload className="h-5 w-5" />
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{trailingActions && <div className="flex items-center gap-2">{trailingActions}</div>}
|
|
|
|
<button
|
|
onClick={isSending ? onStop : onSend}
|
|
disabled={!isSending && !submitEnabled}
|
|
className={`flex items-center gap-2 rounded-xl px-4 py-2 font-medium shadow-sm transition-all ${
|
|
isSending
|
|
? isDarkMode
|
|
? 'bg-rose-600 text-white'
|
|
: 'bg-red-600 text-white'
|
|
: submitEnabled
|
|
? isDarkMode
|
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
: 'bg-red-500 text-white hover:bg-red-600'
|
|
: isDarkMode
|
|
? 'cursor-not-allowed bg-slate-600 text-slate-400'
|
|
: 'cursor-not-allowed bg-gray-300 text-gray-500'
|
|
}`}
|
|
type="button"
|
|
title={isSending ? '停止' : '发送'}
|
|
>
|
|
{isSending ? <Square className="h-4 w-4 fill-current" /> : <Send className="h-4 w-4" />}
|
|
{isSending ? '停止' : '发送'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|