Download components/ChatView.tsx from JeCabrera/ReelBot: direct link, hf CLI and curl.
- Browser
- Download file 9.67 kB
-
https://huggingface.co/spaces/JeCabrera/ReelBot/resolve/main/components/ChatView.tsx
- Command line
-
hf download hf://spaces/JeCabrera/ReelBot/components/ChatView.tsx
-
curl -L -o ChatView.tsx https://huggingface.co/spaces/JeCabrera/ReelBot/resolve/main/components/ChatView.tsx
9.67 kB
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | |
| import type { ChatSession, ChatMessage, UploadedFile } from '../types'; | |
| import { ChatMessageItem } from './ChatMessageItem'; | |
| import { SuggestionButton } from './SuggestionButton'; | |
| import { SendIcon, FilmIcon, PaperclipIcon, XCircleIcon as XCircleIconFile, DocumentTextIcon, StopIcon } from './icons'; | |
| import { SUGGESTION_PROMPTS, REELBOT_IMAGE_URL } from '../constants'; | |
| const MAX_FILE_SIZE_MB = 5; | |
| const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; | |
| const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; | |
| const ALLOWED_DOC_TYPES = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; | |
| const ALLOWED_FILE_TYPES = [...ALLOWED_IMAGE_TYPES, ...ALLOWED_DOC_TYPES]; | |
| interface ChatViewProps { | |
| activeChatSession: ChatSession | null; | |
| onSendMessage: (message: string, file?: UploadedFile, isSuggestion?: boolean) => Promise<void>; | |
| isLoading: boolean; | |
| error: string | null; | |
| onStopGeneration: () => void; | |
| } | |
| export const ChatView: React.FC<ChatViewProps> = ({ activeChatSession, onSendMessage, isLoading, error, onStopGeneration }) => { | |
| const [userInput, setUserInput] = useState(''); | |
| const [selectedFile, setSelectedFile] = useState<UploadedFile | null>(null); | |
| const [fileError, setFileError] = useState<string | null>(null); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }; | |
| useEffect(scrollToBottom, [activeChatSession?.messages]); | |
| useEffect(() => { | |
| if (!isLoading && inputRef.current) { | |
| inputRef.current.focus(); | |
| } | |
| }, [isLoading, activeChatSession]); | |
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| setFileError(null); | |
| setSelectedFile(null); | |
| if (file) { | |
| if (file.size > MAX_FILE_SIZE_BYTES) { | |
| setFileError(`El archivo es demasiado grande (máx. ${MAX_FILE_SIZE_MB}MB).`); | |
| return; | |
| } | |
| if (!ALLOWED_FILE_TYPES.includes(file.type)) { | |
| setFileError('Tipo de archivo no admitido.'); | |
| return; | |
| } | |
| const fileInfo: UploadedFile = { | |
| name: file.name, | |
| type: file.type, | |
| size: file.size, | |
| }; | |
| if (ALLOWED_IMAGE_TYPES.includes(file.type)) { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| fileInfo.dataUrl = reader.result as string; | |
| setSelectedFile(fileInfo); | |
| }; | |
| reader.onerror = () => { | |
| setFileError('Error al leer el archivo de imagen.'); | |
| } | |
| reader.readAsDataURL(file); | |
| } else if (ALLOWED_DOC_TYPES.includes(file.type)) { | |
| setSelectedFile(fileInfo); | |
| } | |
| } | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ""; | |
| } | |
| }; | |
| const clearSelectedFile = () => { | |
| setSelectedFile(null); | |
| setFileError(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ""; | |
| } | |
| }; | |
| const handleSubmit = (e?: React.FormEvent) => { | |
| e?.preventDefault(); | |
| if ((userInput.trim() || selectedFile) && !isLoading) { | |
| onSendMessage(userInput.trim(), selectedFile || undefined); | |
| setUserInput(''); | |
| clearSelectedFile(); | |
| } | |
| }; | |
| const handleSuggestionClick = (promptText: string) => { | |
| if (!isLoading) { | |
| onSendMessage(promptText, undefined, true); | |
| setUserInput(''); | |
| clearSelectedFile(); | |
| } | |
| }; | |
| const lastMessage = activeChatSession?.messages?.[activeChatSession.messages.length - 1]; | |
| const modelIsCurrentlyStreaming = isLoading && lastMessage?.sender === 'model' && lastMessage?.isStreaming; | |
| const modelIsThinking = isLoading && !modelIsCurrentlyStreaming; | |
| let placeholderText = "Describe tu audiencia y el objetivo de tu Reel..."; | |
| if (modelIsThinking) { | |
| placeholderText = "ReelBot está pensando..."; | |
| } else if (modelIsCurrentlyStreaming) { | |
| placeholderText = "ReelBot está escribiendo..."; | |
| } else if (selectedFile) { | |
| placeholderText = "Añade un comentario sobre el archivo..."; | |
| } | |
| return ( | |
| <div className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden"> {/* Added overflow-hidden */} | |
| <div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4"> {/* Adjusted padding for consistency */} | |
| {!activeChatSession || activeChatSession.messages.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-full text-center p-4 md:-translate-y-[30px]"> | |
| <img | |
| src={REELBOT_IMAGE_URL} | |
| alt="ReelBot Logo" | |
| className="w-full max-w-[200px] sm:max-w-[300px] md:max-w-[400px] lg:max-w-[500px] h-auto mb-4 md:mb-6 shadow-lg object-contain" | |
| /> | |
| <h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-cyan-400 mb-2 -mt-2.5"> | |
| Reel Creator AI | |
| </h2> | |
| <p className="text-slate-400 mb-3 text-base sm:text-lg lg:text-xl">By Jesús Cabrera</p> | |
| <p className="text-slate-300 mb-6 md:mb-8 max-w-md sm:max-w-lg md:max-w-xl lg:max-w-2xl text-lg sm:text-xl lg:text-2xl flex items-center justify-center"> | |
| <FilmIcon className="w-5 h-5 mr-2 flex-shrink-0" /> | |
| Experto en crear Reels virales que convierten visualizaciones en clientes | |
| </p> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 w-full max-w-2xl"> | |
| {SUGGESTION_PROMPTS.map((prompt) => ( | |
| <SuggestionButton | |
| key={prompt.text} | |
| text={prompt.text} | |
| // emoji prop removed | |
| onClick={() => handleSuggestionClick(prompt.text)} | |
| disabled={isLoading} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| activeChatSession.messages.map((msg) => ( | |
| <ChatMessageItem key={msg.id} message={msg} /> | |
| )) | |
| )} | |
| <div ref={messagesEndRef} /> | |
| {error && <div className="text-red-400 p-2 bg-red-900/50 rounded-md text-sm">{error}</div>} | |
| </div> | |
| <div className="p-4 border-t border-slate-700 bg-slate-900"> {/* Removed sticky and z-index, handled by App.tsx structure */} | |
| {selectedFile && ( | |
| <div className="mb-2 p-2 bg-slate-800 rounded-md flex items-center justify-between"> | |
| <div className="flex items-center space-x-2 overflow-hidden"> | |
| {selectedFile.dataUrl && ALLOWED_IMAGE_TYPES.includes(selectedFile.type) ? ( | |
| <img src={selectedFile.dataUrl} alt="Preview" className="w-10 h-10 rounded object-cover" /> | |
| ) : ( | |
| <DocumentTextIcon className="w-8 h-8 text-slate-400 flex-shrink-0" /> | |
| )} | |
| <span className="text-xs text-slate-300 truncate" title={selectedFile.name}> | |
| {selectedFile.name} | |
| </span> | |
| </div> | |
| <button onClick={clearSelectedFile} className="text-slate-400 hover:text-slate-200" disabled={isLoading}> | |
| <XCircleIconFile className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| )} | |
| {fileError && <p className="text-red-400 text-xs mb-2">{fileError}</p>} | |
| <form onSubmit={handleSubmit} className="flex items-center space-x-3"> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| onChange={handleFileChange} | |
| className="hidden" | |
| accept={ALLOWED_FILE_TYPES.join(',')} | |
| disabled={isLoading} | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => fileInputRef.current?.click()} | |
| disabled={isLoading} | |
| className="p-3 bg-slate-700 hover:bg-slate-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-slate-300 hover:text-slate-100 transition-colors duration-150 focus:ring-2 focus:ring-cyan-500 focus:outline-none" | |
| aria-label="Attach file" | |
| > | |
| <PaperclipIcon className="w-5 h-5" /> | |
| </button> | |
| <input | |
| ref={inputRef} | |
| type="text" | |
| value={userInput} | |
| onChange={(e) => setUserInput(e.target.value)} | |
| placeholder={placeholderText} | |
| className="flex-1 p-3 bg-slate-800 border border-slate-700 rounded-lg text-slate-100 placeholder-slate-500 focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 outline-none transition-shadow duration-150 disabled:opacity-70" | |
| disabled={isLoading} | |
| /> | |
| {isLoading ? ( | |
| <button | |
| type="button" | |
| onClick={onStopGeneration} | |
| className="p-3 bg-red-600 hover:bg-red-500 rounded-lg text-white transition-colors duration-150 focus:ring-2 focus:ring-red-400 focus:outline-none" | |
| aria-label="Stop generation" | |
| > | |
| <StopIcon className="w-5 h-5" /> | |
| </button> | |
| ) : ( | |
| <button | |
| type="submit" | |
| disabled={(!userInput.trim() && !selectedFile) || isLoading} | |
| className="p-3 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-600 disabled:cursor-not-allowed rounded-lg text-white transition-colors duration-150 focus:ring-2 focus:ring-cyan-400 focus:outline-none" | |
| aria-label="Send message" | |
| > | |
| <SendIcon className="w-5 h-5" /> | |
| </button> | |
| )} | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| }; | |