2026-01-04 00:41:51 +00:00
|
|
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
|
|
import { Send, Smile, Paperclip, X, Image as ImageIcon, File } from 'lucide-react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { useChat } from '../hooks/useChat';
|
|
|
|
|
import { useChatStore } from '../store/chatStore';
|
2026-01-04 00:41:51 +00:00
|
|
|
import EmojiPicker, { Theme } from 'emoji-picker-react';
|
|
|
|
|
import { useDropzone } from 'react-dropzone';
|
|
|
|
|
import { apiClient } from '@/services/api/client';
|
|
|
|
|
import { MessageAttachment } from '../types';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
export const ChatInput: React.FC = () => {
|
|
|
|
|
const [message, setMessage] = useState('');
|
2026-01-04 00:41:51 +00:00
|
|
|
const [attachments, setAttachments] = useState<MessageAttachment[]>([]);
|
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
|
|
|
const { sendMessage, setTyping } = useChat();
|
2025-12-03 21:56:50 +00:00
|
|
|
const { currentConversationId } = useChatStore();
|
2026-01-04 00:41:51 +00:00
|
|
|
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
2026-01-04 00:41:51 +00:00
|
|
|
if ((message.trim() || attachments.length > 0) && currentConversationId) {
|
|
|
|
|
sendMessage(message, attachments.length > 0 ? attachments : undefined);
|
2025-12-03 21:56:50 +00:00
|
|
|
setMessage('');
|
2026-01-04 00:41:51 +00:00
|
|
|
setAttachments([]);
|
|
|
|
|
|
|
|
|
|
// Stop typing indicator immediately
|
|
|
|
|
if (typingTimeoutRef.current) {
|
|
|
|
|
clearTimeout(typingTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
setTyping(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
try {
|
|
|
|
|
const uploadPromises = acceptedFiles.map(async (file) => {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
// Use existing upload endpoint
|
|
|
|
|
const response = await apiClient.post('/uploads', formData, {
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = response.data;
|
|
|
|
|
return {
|
|
|
|
|
file_name: file.name,
|
|
|
|
|
file_type: file.type,
|
|
|
|
|
file_url: data.url, // Assuming backend returns { url: "..." }
|
|
|
|
|
file_size: file.size,
|
|
|
|
|
} as MessageAttachment;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const newAttachments = await Promise.all(uploadPromises);
|
|
|
|
|
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to upload files:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
|
|
|
onDrop,
|
|
|
|
|
noClick: true, // We want custom click on button
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleEmojiClick = (emojiData: { emoji: string }) => {
|
|
|
|
|
setMessage((prev) => prev + emojiData.emoji);
|
|
|
|
|
setShowEmojiPicker(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileButtonClick = () => {
|
|
|
|
|
fileInputRef.current?.click();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
if (e.target.files) {
|
|
|
|
|
onDrop(Array.from(e.target.files));
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-04 00:41:51 +00:00
|
|
|
const removeAttachment = (index: number) => {
|
|
|
|
|
setAttachments((prev) => prev.filter((_, i) => i !== index));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Typing indicator logic
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (message.length > 0) {
|
|
|
|
|
setTyping(true);
|
|
|
|
|
|
|
|
|
|
if (typingTimeoutRef.current) {
|
|
|
|
|
clearTimeout(typingTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typingTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
setTyping(false);
|
|
|
|
|
}, 3000); // Stop typing after 3 seconds of inactivity
|
|
|
|
|
} else {
|
|
|
|
|
setTyping(false);
|
|
|
|
|
}
|
|
|
|
|
}, [message, setTyping]);
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return (
|
2026-01-04 00:41:51 +00:00
|
|
|
<div {...getRootProps()} className="border-t bg-gray-50">
|
2025-12-03 21:56:50 +00:00
|
|
|
<input
|
2026-01-04 00:41:51 +00:00
|
|
|
{...getInputProps()}
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
onChange={handleFileChange}
|
|
|
|
|
className="hidden"
|
2025-12-03 21:56:50 +00:00
|
|
|
/>
|
2026-01-04 00:41:51 +00:00
|
|
|
|
|
|
|
|
{/* File Previews */}
|
|
|
|
|
{attachments.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-2 p-2 px-4 border-b bg-white">
|
|
|
|
|
{attachments.map((att, i) => (
|
|
|
|
|
<div key={i} className="relative group flex items-center gap-2 p-1.5 bg-gray-100 rounded-md border text-xs">
|
|
|
|
|
{att.file_type.startsWith('image') ? (
|
|
|
|
|
<ImageIcon size={14} className="text-blue-500" />
|
|
|
|
|
) : (
|
|
|
|
|
<File size={14} className="text-gray-500" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="truncate max-w-[100px]">{att.file_name}</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => removeAttachment(i)}
|
|
|
|
|
className="p-0.5 hover:bg-gray-200 rounded-full"
|
|
|
|
|
>
|
|
|
|
|
<X size={12} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{isDragActive && (
|
|
|
|
|
<div className="absolute inset-0 z-50 bg-blue-500/10 flex items-center justify-center border-2 border-dashed border-blue-500 pointer-events-none">
|
|
|
|
|
<p className="text-blue-600 font-semibold">Déposez vos fichiers ici</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
className="flex items-center gap-2 p-3"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
2026-01-04 00:41:51 +00:00
|
|
|
<div className="flex gap-1">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="p-2 text-gray-500 hover:bg-gray-200 rounded-lg transition-colors"
|
|
|
|
|
onClick={handleFileButtonClick}
|
|
|
|
|
>
|
|
|
|
|
<Paperclip size={20} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={cn(
|
|
|
|
|
"p-2 text-gray-500 hover:bg-gray-200 rounded-lg transition-colors",
|
|
|
|
|
showEmojiPicker && "bg-gray-200 text-blue-600"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
|
|
|
>
|
|
|
|
|
<Smile size={20} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{showEmojiPicker && (
|
|
|
|
|
<div className="absolute bottom-full left-0 mb-2 z-50">
|
|
|
|
|
<div className="fixed inset-0" onClick={() => setShowEmojiPicker(false)} />
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<EmojiPicker
|
|
|
|
|
onEmojiClick={handleEmojiClick}
|
|
|
|
|
theme={Theme.LIGHT}
|
|
|
|
|
lazyLoadEmojis={true}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={message}
|
|
|
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
|
|
|
placeholder="Écrire un message..."
|
|
|
|
|
className="flex-1 p-2 bg-white border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
|
|
|
|
disabled={!currentConversationId || isUploading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
|
|
|
|
disabled={!currentConversationId || (!message.trim() && attachments.length === 0) || isUploading}
|
|
|
|
|
>
|
|
|
|
|
{isUploading ? (
|
|
|
|
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Send size={20} />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
2025-12-13 02:34:34 +00:00
|
|
|
};
|
2026-01-04 00:41:51 +00:00
|
|
|
|
|
|
|
|
// Helper for class names since Lucide and ShadUI might be used
|
|
|
|
|
function cn(...classes: (string | boolean | undefined)[]) {
|
|
|
|
|
return classes.filter(Boolean).join(' ');
|
|
|
|
|
}
|