|
|
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; |
|
|
|