File size: 9,670 Bytes
2accaeb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
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>
);
};
|