UI / frontend /components /ChatInput.tsx
Chan-Y's picture
Initial commit for HF Space
a94ab76
import React, { useState, KeyboardEvent, useRef } from 'react';
import { Send, Loader2, Plus, X } from 'lucide-react';
interface ChatInputProps {
onSend: (message: string) => void;
disabled?: boolean;
isLoading?: boolean;
onImageSelect?: (image: string | null) => void;
currentImage?: string | null;
supportsImages?: boolean;
}
export const ChatInput: React.FC<ChatInputProps> = ({
onSend,
disabled = false,
isLoading = false,
onImageSelect,
currentImage,
supportsImages = false
}) => {
const [input, setInput] = useState('');
const [preview, setPreview] = useState<string | null>(currentImage || null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleSend = () => {
if (input.trim() && !disabled && !isLoading) {
onSend(input.trim());
setInput('');
}
};
const handleKeyPress = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
setPreview(result);
onImageSelect?.(result);
};
reader.readAsDataURL(file);
}
};
const handleRemoveImage = () => {
setPreview(null);
onImageSelect?.(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
// Update preview when currentImage changes
React.useEffect(() => {
setPreview(currentImage || null);
}, [currentImage]);
return (
<div className="w-full max-w-4xl mx-auto">
{/* Image Preview */}
{preview && (
<div className="mb-3 relative inline-block">
<img
src={preview}
alt="Preview"
className="max-w-xs h-32 object-contain rounded-lg border border-gray-300 dark:border-gray-700"
/>
<button
onClick={handleRemoveImage}
className="absolute -top-2 -right-2 p-1 bg-red-500 hover:bg-red-600 text-white rounded-full transition-all shadow-lg"
>
<X size={14} />
</button>
</div>
)}
<div className="relative flex items-center gap-2 bg-white dark:bg-gray-800 rounded-3xl shadow-lg border border-gray-200 dark:border-gray-700 px-3 py-3 transition-all focus-within:shadow-xl focus-within:border-primary-500 dark:focus-within:border-primary-600">
{/* Hidden File Input */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
disabled={!supportsImages}
/>
{/* Plus Button for Image Upload */}
<button
onClick={() => fileInputRef.current?.click()}
disabled={!supportsImages}
className={`flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center transition-all ${
supportsImages
? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
: 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
}`}
title={supportsImages ? "Resim yükle" : "Bu model resim desteklemiyor"}
>
<Plus size={20} />
</button>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Herhangi bir şey sor"
rows={1}
disabled={disabled || isLoading}
className="flex-1 bg-transparent text-gray-900 dark:text-gray-100 resize-none border-none focus:outline-none focus:ring-0 placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:cursor-not-allowed"
style={{ minHeight: '24px', maxHeight: '120px' }}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = 'auto';
target.style.height = Math.min(target.scrollHeight, 120) + 'px';
}}
/>
<button
onClick={handleSend}
disabled={!input.trim() || disabled || isLoading}
className="flex-shrink-0 w-8 h-8 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed rounded-lg flex items-center justify-center transition-all"
>
{isLoading ? (
<Loader2 size={18} className="animate-spin text-gray-600 dark:text-gray-400" />
) : (
<Send size={18} className="text-gray-600 dark:text-gray-300" />
)}
</button>
</div>
</div>
);
};