| import React, { useState, useRef, useEffect } from "react"; |
|
|
| interface Message { |
| role: "user" | "assistant"; |
| content: string; |
| } |
|
|
| interface ChatInterfaceProps { |
| pipelineId: string | null; |
| onFileUpload: (file: File) => Promise<void>; |
| } |
|
|
| const TypingIndicator = () => ( |
| <div className="flex justify-start"> |
| <div className="bg-gray-800 text-gray-100 p-4 rounded-lg flex items-center space-x-2"> |
| <div |
| className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" |
| style={{ animationDelay: "0ms" }} |
| ></div> |
| <div |
| className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" |
| style={{ animationDelay: "150ms" }} |
| ></div> |
| <div |
| className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" |
| style={{ animationDelay: "300ms" }} |
| ></div> |
| </div> |
| </div> |
| ); |
|
|
| const LoadingState = () => ( |
| <div className="flex flex-col items-center justify-center h-full"> |
| <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full"> |
| <div className="flex items-center justify-center mb-6"> |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> |
| </div> |
| <div className="space-y-4"> |
| <div className="h-4 bg-gray-700 rounded animate-pulse"></div> |
| <div className="h-4 bg-gray-700 rounded animate-pulse w-3/4"></div> |
| <div className="h-4 bg-gray-700 rounded animate-pulse w-1/2"></div> |
| </div> |
| <div className="mt-6 text-gray-400 text-center"> |
| Processing your document... |
| </div> |
| </div> |
| </div> |
| ); |
|
|
| const ErrorState = ({ onRetry }: { onRetry: () => void }) => ( |
| <div className="flex flex-col items-center justify-center h-full"> |
| <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full"> |
| <div className="flex items-center justify-center mb-6 text-red-500"> |
| <svg |
| className="w-12 h-12" |
| fill="none" |
| stroke="currentColor" |
| viewBox="0 0 24 24" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| 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" |
| /> |
| </svg> |
| </div> |
| <div className="text-center"> |
| <h3 className="text-lg font-medium text-white mb-2"> |
| Document Upload Failed |
| </h3> |
| <p className="text-gray-400 mb-6"> |
| There was an error processing your document. Please try again. |
| </p> |
| <button |
| onClick={onRetry} |
| className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" |
| > |
| Try Again |
| </button> |
| </div> |
| </div> |
| </div> |
| ); |
|
|
| const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || ""; |
|
|
| const ChatInterface: React.FC<ChatInterfaceProps> = ({ |
| pipelineId, |
| onFileUpload, |
| }) => { |
| const [messages, setMessages] = useState<Message[]>([]); |
| const [input, setInput] = useState(""); |
| const [isLoading, setIsLoading] = useState(false); |
| const [isUploading, setIsUploading] = useState(false); |
| const [uploadError, setUploadError] = useState(false); |
| const messagesEndRef = useRef<null | HTMLDivElement>(null); |
| const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
| useEffect(() => { |
| if (pipelineId) { |
| setIsUploading(false); |
| setMessages([ |
| { |
| role: "assistant", |
| content: |
| "👋 Your document has been processed! You can now ask questions about its content.", |
| }, |
| ]); |
| } |
| }, [pipelineId]); |
|
|
| const scrollToBottom = () => { |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }; |
|
|
| useEffect(() => { |
| scrollToBottom(); |
| }, [messages]); |
|
|
| const handleFileUpload = async ( |
| event: React.ChangeEvent<HTMLInputElement> |
| ) => { |
| const file = event.target.files?.[0]; |
| if (file) { |
| try { |
| setIsUploading(true); |
| setUploadError(false); |
| await onFileUpload(file); |
| } catch (error) { |
| console.error("Error uploading file:", error); |
| setUploadError(true); |
| } finally { |
| setIsUploading(false); |
| } |
| } |
| }; |
|
|
| const handleRetry = () => { |
| setUploadError(false); |
| |
| if (fileInputRef.current) { |
| fileInputRef.current.value = ""; |
| } |
| }; |
|
|
| const sendMessage = async (e: React.FormEvent) => { |
| e.preventDefault(); |
| if (!input.trim() || !pipelineId) return; |
|
|
| const userMessage = input.trim(); |
| setInput(""); |
| setMessages((prev) => [...prev, { role: "user", content: userMessage }]); |
| setIsLoading(true); |
|
|
| try { |
| const response = await fetch(`${API_BASE_URL}/api/chat/${pipelineId}`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ query: userMessage }), |
| }); |
|
|
| const data = await response.json(); |
| setMessages((prev) => [ |
| ...prev, |
| { role: "assistant", content: data.response }, |
| ]); |
| } catch (error) { |
| console.error("Error sending message:", error); |
| setMessages((prev) => [ |
| ...prev, |
| { role: "assistant", content: "Error: Failed to get response" }, |
| ]); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| return ( |
| <div className="flex flex-col h-screen bg-gray-900"> |
| {isUploading ? ( |
| <LoadingState /> |
| ) : uploadError ? ( |
| <ErrorState onRetry={handleRetry} /> |
| ) : !pipelineId ? ( |
| <div className="flex flex-col items-center justify-center h-full"> |
| <div className="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full"> |
| <h2 className="text-2xl font-bold text-white mb-6 text-center"> |
| Upload your document |
| </h2> |
| <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"> |
| <div className="flex flex-col items-center justify-center pt-5 pb-6"> |
| <svg |
| className="w-8 h-8 mb-4 text-gray-400" |
| aria-hidden="true" |
| xmlns="http://www.w3.org/2000/svg" |
| fill="none" |
| viewBox="0 0 20 16" |
| > |
| <path |
| stroke="currentColor" |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth="2" |
| 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" |
| /> |
| </svg> |
| <p className="mb-2 text-sm text-gray-400"> |
| <span className="font-semibold">Click to upload</span> or drag |
| and drop |
| </p> |
| <p className="text-xs text-gray-400">PDF or TXT files</p> |
| </div> |
| <input |
| ref={fileInputRef} |
| type="file" |
| accept=".pdf,.txt" |
| onChange={handleFileUpload} |
| className="hidden" |
| /> |
| </label> |
| </div> |
| </div> |
| ) : ( |
| <div className="flex flex-col h-full max-w-5xl mx-auto w-full"> |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 mb-6"> |
| {messages.map((message, index) => ( |
| <div |
| key={index} |
| className={`flex ${ |
| message.role === "user" ? "justify-end" : "justify-start" |
| }`} |
| > |
| <div |
| className={`max-w-[80%] p-4 rounded-lg shadow-lg ${ |
| message.role === "user" |
| ? "bg-blue-600 text-white" |
| : "bg-gray-800 text-gray-100" |
| }`} |
| > |
| {message.content} |
| </div> |
| </div> |
| ))} |
| {isLoading && <TypingIndicator />} |
| {messages.length === 0 && !isLoading && ( |
| <div className="flex justify-center items-center h-full"> |
| <div className="text-gray-400 text-center"> |
| <p>No messages yet</p> |
| <p className="text-sm mt-2"> |
| Start by asking a question about your document |
| </p> |
| </div> |
| </div> |
| )} |
| <div ref={messagesEndRef} /> |
| </div> |
| <div className="px-4 pb-6"> |
| <div className="bg-gray-800 rounded-lg p-2 shadow-lg border border-gray-700"> |
| <form onSubmit={sendMessage} className="flex items-center gap-2"> |
| <input |
| type="text" |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| placeholder="Ask a question about your document..." |
| className="flex-1 p-3 bg-transparent text-white placeholder-gray-400 focus:outline-none" |
| disabled={isLoading} |
| /> |
| <button |
| type="submit" |
| disabled={isLoading || !input.trim()} |
| 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" |
| > |
| {isLoading ? ( |
| <> |
| <svg |
| className="animate-spin h-4 w-4 text-white" |
| xmlns="http://www.w3.org/2000/svg" |
| fill="none" |
| viewBox="0 0 24 24" |
| > |
| <circle |
| className="opacity-25" |
| cx="12" |
| cy="12" |
| r="10" |
| stroke="currentColor" |
| strokeWidth="4" |
| ></circle> |
| <path |
| className="opacity-75" |
| fill="currentColor" |
| 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" |
| ></path> |
| </svg> |
| <span>Sending...</span> |
| </> |
| ) : ( |
| <> |
| <span>Send</span> |
| <svg |
| className="w-4 h-4" |
| fill="none" |
| stroke="currentColor" |
| viewBox="0 0 24 24" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| d="M14 5l7 7m0 0l-7 7m7-7H3" |
| /> |
| </svg> |
| </> |
| )} |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default ChatInterface; |
|
|