itsprasun/pythonic-rag-fastapi-react
0
1import React, { useState, useRef, useEffect } from "react";2 3interface Message {4 role: "user" | "assistant";5 content: string;6}7 8interface ChatInterfaceProps {9 pipelineId: string | null;10 onFileUpload: (file: File) => Promise<void>;11}12 13const TypingIndicator = () => (14 <div className="flex justify-start">15 <div className="bg-gray-800 text-gray-100 p-4 rounded-lg flex items-center space-x-2">16 <div17 className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"18 style={{ animationDelay: "0ms" }}19 ></div>20 <div21 className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"22 style={{ animationDelay: "150ms" }}23 ></div>24 <div25 className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"26 style={{ animationDelay: "300ms" }}27 ></div>28 </div>29 </div>30);31 32const LoadingState = () => (33 <div className="flex flex-col items-center justify-center h-full">34 <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full">35 <div className="flex items-center justify-center mb-6">36 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>37 </div>38 <div className="space-y-4">39 <div className="h-4 bg-gray-700 rounded animate-pulse"></div>40 <div className="h-4 bg-gray-700 rounded animate-pulse w-3/4"></div>41 <div className="h-4 bg-gray-700 rounded animate-pulse w-1/2"></div>42 </div>43 <div className="mt-6 text-gray-400 text-center">44 Processing your document...45 </div>46 </div>47 </div>48);49 50const ErrorState = ({ onRetry }: { onRetry: () => void }) => (51 <div className="flex flex-col items-center justify-center h-full">52 <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full">53 <div className="flex items-center justify-center mb-6 text-red-500">54 <svg55 className="w-12 h-12"56 fill="none"57 stroke="currentColor"58 viewBox="0 0 24 24"59 >60 <path61 strokeLinecap="round"62 strokeLinejoin="round"63 strokeWidth={2}64 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"65 />66 </svg>67 </div>68 <div className="text-center">69 <h3 className="text-lg font-medium text-white mb-2">70 Document Upload Failed71 </h3>72 <p className="text-gray-400 mb-6">73 There was an error processing your document. Please try again.74 </p>75 <button76 onClick={onRetry}77 className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"78 >79 Try Again80 </button>81 </div>82 </div>83 </div>84);85 86const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "";87 88const ChatInterface: React.FC<ChatInterfaceProps> = ({89 pipelineId,90 onFileUpload,91}) => {92 const [messages, setMessages] = useState<Message[]>([]);93 const [input, setInput] = useState("");94 const [isLoading, setIsLoading] = useState(false);95 const [isUploading, setIsUploading] = useState(false);96 const [uploadError, setUploadError] = useState(false);97 const messagesEndRef = useRef<null | HTMLDivElement>(null);98 const fileInputRef = useRef<HTMLInputElement>(null);99 100 useEffect(() => {101 if (pipelineId) {102 setIsUploading(false);103 setMessages([104 {105 role: "assistant",106 content:107 "๐ Your document has been processed! You can now ask questions about its content.",108 },109 ]);110 }111 }, [pipelineId]);112 113 const scrollToBottom = () => {114 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });115 };116 117 useEffect(() => {118 scrollToBottom();119 }, [messages]);120 121 const handleFileUpload = async (122 event: React.ChangeEvent<HTMLInputElement>123 ) => {124 const file = event.target.files?.[0];125 if (file) {126 try {127 setIsUploading(true);128 setUploadError(false);129 await onFileUpload(file);130 } catch (error) {131 console.error("Error uploading file:", error);132 setUploadError(true);133 } finally {134 setIsUploading(false);135 }136 }137 };138 139 const handleRetry = () => {140 setUploadError(false);141 // Reset file input142 if (fileInputRef.current) {143 fileInputRef.current.value = "";144 }145 };146 147 const sendMessage = async (e: React.FormEvent) => {148 e.preventDefault();149 if (!input.trim() || !pipelineId) return;150 151 const userMessage = input.trim();152 setInput("");153 setMessages((prev) => [...prev, { role: "user", content: userMessage }]);154 setIsLoading(true);155 156 try {157 const response = await fetch(`${API_BASE_URL}/api/chat/${pipelineId}`, {158 method: "POST",159 headers: {160 "Content-Type": "application/json",161 },162 body: JSON.stringify({ query: userMessage }),163 });164 165 const data = await response.json();166 setMessages((prev) => [167 ...prev,168 { role: "assistant", content: data.response },169 ]);170 } catch (error) {171 console.error("Error sending message:", error);172 setMessages((prev) => [173 ...prev,174 { role: "assistant", content: "Error: Failed to get response" },175 ]);176 } finally {177 setIsLoading(false);178 }179 };180 181 return (182 <div className="flex flex-col h-screen bg-gray-900">183 {isUploading ? (184 <LoadingState />185 ) : uploadError ? (186 <ErrorState onRetry={handleRetry} />187 ) : !pipelineId ? (188 <div className="flex flex-col items-center justify-center h-full">189 <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full">190 <h2 className="text-2xl font-bold text-white mb-6 text-center">191 Upload your document192 </h2>193 <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-600 border-dashed rounded-lg cursor-pointer hover:border-gray-500 hover:bg-gray-800/50 transition-all">194 <div className="flex flex-col items-center justify-center pt-5 pb-6">195 <svg196 className="w-8 h-8 mb-4 text-gray-400"197 aria-hidden="true"198 xmlns="http://www.w3.org/2000/svg"199 fill="none"200 viewBox="0 0 20 16"201 >202 <path203 stroke="currentColor"204 strokeLinecap="round"205 strokeLinejoin="round"206 strokeWidth="2"207 d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"208 />209 </svg>210 <p className="mb-2 text-sm text-gray-400">211 <span className="font-semibold">Click to upload</span> or drag212 and drop213 </p>214 <p className="text-xs text-gray-400">PDF or TXT files</p>215 </div>216 <input217 ref={fileInputRef}218 type="file"219 accept=".pdf,.txt"220 onChange={handleFileUpload}221 className="hidden"222 />223 </label>224 </div>225 </div>226 ) : (227 <div className="flex flex-col h-full max-w-5xl mx-auto w-full">228 <div className="flex-1 overflow-y-auto p-4 space-y-4 mb-6">229 {messages.map((message, index) => (230 <div231 key={index}232 className={`flex ${233 message.role === "user" ? "justify-end" : "justify-start"234 }`}235 >236 <div237 className={`max-w-[80%] p-4 rounded-lg shadow-lg ${238 message.role === "user"239 ? "bg-blue-600 text-white"240 : "bg-gray-800 text-gray-100"241 }`}242 >243 {message.content}244 </div>245 </div>246 ))}247 {isLoading && <TypingIndicator />}248 {messages.length === 0 && !isLoading && (249 <div className="flex justify-center items-center h-full">250 <div className="text-gray-400 text-center">251 <p>No messages yet</p>252 <p className="text-sm mt-2">253 Start by asking a question about your document254 </p>255 </div>256 </div>257 )}258 <div ref={messagesEndRef} />259 </div>260 <div className="px-4 pb-6">261 <div className="bg-gray-800 rounded-lg p-2 shadow-lg border border-gray-700">262 <form onSubmit={sendMessage} className="flex items-center gap-2">263 <input264 type="text"265 value={input}266 onChange={(e) => setInput(e.target.value)}267 placeholder="Ask a question about your document..."268 className="flex-1 p-3 bg-transparent text-white placeholder-gray-400 focus:outline-none"269 disabled={isLoading}270 />271 <button272 type="submit"273 disabled={isLoading || !input.trim()}274 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors flex items-center gap-2 min-w-[100px] justify-center"275 >276 {isLoading ? (277 <>278 <svg279 className="animate-spin h-4 w-4 text-white"280 xmlns="http://www.w3.org/2000/svg"281 fill="none"282 viewBox="0 0 24 24"283 >284 <circle285 className="opacity-25"286 cx="12"287 cy="12"288 r="10"289 stroke="currentColor"290 strokeWidth="4"291 ></circle>292 <path293 className="opacity-75"294 fill="currentColor"295 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"296 ></path>297 </svg>298 <span>Sending...</span>299 </>300 ) : (301 <>302 <span>Send</span>303 <svg304 className="w-4 h-4"305 fill="none"306 stroke="currentColor"307 viewBox="0 0 24 24"308 >309 <path310 strokeLinecap="round"311 strokeLinejoin="round"312 strokeWidth={2}313 d="M14 5l7 7m0 0l-7 7m7-7H3"314 />315 </svg>316 </>317 )}318 </button>319 </form>320 </div>321 </div>322 </div>323 )}324 </div>325 );326};327 328export default ChatInterface;329 