import React, { useState, useEffect, useRef } from "react"; import ReactMarkdown from "react-markdown"; import FileUploadCard from "./FileUploadCard"; const DeleteBinButton = ({ onClick }) => ( ); export default function App() { // <-- Set your backend URL here (the .hf.space backend) const API_BASE = "https://durga-7780-chatbot.hf.space"; const [allChats, setAllChats] = useState({}); const [messages, setMessages] = useState([]); // {id, role, text} const [input, setInput] = useState(""); const [uploading, setUploading] = useState(false); const [files, setFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState({}); const [sessionId, setSessionId] = useState(() => { return localStorage.getItem("rag_session") || Math.random().toString(36).slice(2, 10); }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isRecording, setIsRecording] = useState(false); const [isTranscribing, setIsTranscribing] = useState(false); const messagesEndRef = useRef(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const justTranscribedRef = useRef(false); const [chatHistory, setChatHistory] = useState([]); const [warning, setWarning] = useState(""); const [sttCooldown, setSttCooldown] = useState(false); useEffect(() => { const savedChats = JSON.parse(localStorage.getItem("allChats") || "{}"); const savedHistory = JSON.parse(localStorage.getItem("chatHistory") || "[]"); setAllChats(savedChats); setChatHistory(savedHistory); if (sessionId && savedChats[sessionId]) { setMessages(savedChats[sessionId]); } }, []); useEffect(() => { localStorage.setItem("allChats", JSON.stringify(allChats)); }, [allChats]); useEffect(() => { localStorage.setItem("chatHistory", JSON.stringify(chatHistory)); }, [chatHistory]); useEffect(() => { localStorage.setItem("rag_session", sessionId); }, [sessionId]); useEffect(() => { scrollToBottom(); }, [messages, loading]); useEffect(() => { // Inject minimal CSS const css = ` :root{ --bg:#0f1724; --card:#0b1220; --accent:#6ee7b7; --muted:#94a3b8; } *{box-sizing:border-box} html,body,#root{height:100%;} body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial;background:linear-gradient(180deg,#071029 0%, #071b2e 100%);color:#e6eef8} .app{display:flex;height:100vh;padding:24px;gap:24px} .left{width:360px;background:rgba(255,255,255,0.03);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px} .brand{font-weight:700;font-size:18px} .upload-area{background:rgba(255,255,255,0.02);padding:12px;border-radius:8px;border:1px dashed rgba(255,255,255,0.03);display:flex;flex-direction:column;gap:8px} .files-list{display:flex;flex-direction:column;gap:6px;max-height:220px;overflow:auto} .file-item{display:flex;justify-content:space-between;align-items:center;padding:8px;border-radius:8px;background:rgba(255,255,255,0.01)} .btn{background:var(--accent);color:#042024;padding:8px 12px;border-radius:8px;border:0;cursor:pointer;font-weight:600} .btn:disabled{opacity:0.5;cursor:not-allowed} .right{flex:1;display:flex;flex-direction:column;border-radius:12px;background:rgba(255,255,255,0.02);padding:12px;gap:12px} .chat-window{flex:1;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px} .message-row{display:flex;width:100%} /* user RIGHT, ai LEFT */ .message-row.user{justify-content:flex-end} .message-row.ai{justify-content:flex-start} .bubble { max-width: 70%; padding: 8px 10px; border-radius: 12px; box-shadow: 0 4px 14px rgba(2,6,23,0.6); line-height: 1.5; word-break: break-word; } /* user bubble appears on right (bright) */ .bubble.user { background: #2563eb; color: white; margin-left: auto; } /* ai bubble appears on left (dark) */ .bubble.ai{ background:#0f1724; color:#e6eef8; border-top-right-radius:12px; border-top-left-radius:4px; } .text-input { flex: 1; min-height: 44px; max-height: 120px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #e6eef8; font-size: 14px; resize: none; } .text-input::placeholder { color: rgba(255,255,255,0.5); } .input-row { display: flex; align-items: flex-end; gap: 10px; } .switch { position: relative; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; background-color: rgb(60,64,67); color: #fff; border-radius: 50%; cursor: pointer; transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .mic-on, .mic-off { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; transition: all .3s ease-in-out; } .mic-on { z-index: 4; } .mic-off { position: absolute; inset: 0; z-index: 5; opacity: 0; } .switch:hover { background-color: rgba(60,64,67,0.8); } #mic-toggle { display: none; } #mic-toggle:checked + .switch { background-color: red; } #mic-toggle:checked + .switch .mic-off { opacity: 1; } #mic-toggle:checked + .switch .mic-on { opacity: 0; } #mic-toggle:active + .switch { scale: 1.2; } .history-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px; border-radius: 6px; margin-bottom: 6px; cursor: pointer; transition: background 0.2s ease; } .history-item:hover { background: rgba(255,255,255,0.08); } .history-title { flex: 1; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-delete { flex-shrink: 0; } .history-item { min-height: 36px; } .history-title { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .delete-bin-btn { width: 28px; height: 28px; border-radius: 6px; border: none; background: rgba(255, 95, 95, 0.15); cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 0; } .delete-bin-btn:hover { background: rgba(255, 95, 95, 0.35); } .delete-bin-btn .svgIcon { width: 10px; transition: 0.3s; } .delete-bin-btn .svgIcon path { fill: white; } .delete-bin-btn .bin-top { transform-origin: bottom right; } .delete-bin-btn:hover .bin-top { transform: rotate(160deg); } .delete-bin-btn:focus-visible { outline: 2px solid #ff5f5f; outline-offset: 2px; } .delete-bin-btn { background: rgba(255, 0, 0, 0.25); } .delete-bin-btn:hover { background: rgba(255, 0, 0, 0.6); } .delete-bin-btn .svgIcon path { fill: #ff3b3b; } .delete-bin-btn:hover .svgIcon path { fill: #ffffff; } .chat-history-scroll { overflow-y: auto; flex: 1; padding-right: 4px; max-height: calc(100vh - 420px); } `; const style = document.createElement("style"); style.innerHTML = css; document.head.appendChild(style); return () => { if (style && style.parentNode) style.parentNode.removeChild(style); }; }, []); function scrollToBottom() { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } } const handleFiles = (e) => { const fileList = Array.from(e.target.files); setFiles((prev) => [...prev, ...fileList]); }; const removeFile = (index) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }; const loadChat = (id) => { setSessionId(id); const msgs = allChats[id] || []; setMessages(msgs); localStorage.setItem("rag_session", id); }; const uploadFiles = async () => { if (!files.length) return; setUploading(true); setError(null); try { const form = new FormData(); files.forEach((f) => form.append("files", f)); form.append("session_id", sessionId); const xhr = new XMLHttpRequest(); xhr.open("POST", `${API_BASE}/api/upload`); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); // Update progress for all files equally (or enhance per-file) const newProgress = {}; files.forEach((f) => { newProgress[f.name] = percent; }); setUploadProgress(newProgress); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { const data = JSON.parse(xhr.responseText); addSystemMessage( `Uploaded ${data.indexed_files?.length || files.length} file(s). Indexed successfully.` ); setFiles([]); setUploadProgress({}); } else { throw new Error(xhr.responseText || `HTTP ${xhr.status}`); } setUploading(false); }; xhr.onerror = () => { setError("Upload failed"); setUploading(false); }; xhr.send(form); } catch (err) { console.error(err); setError(err.message || "Upload failed"); setUploading(false); } }; const addSystemMessage = (text) => { const msg = { id: Date.now() + Math.random(), role: "system", text }; setMessages((m) => [...m, msg]); setAllChats(prev => ({ ...prev, [sessionId]: [...(prev[sessionId] || []), msg] })); }; const sendMessage = async () => { if (!input.trim()) { setWarning("Please enter a message before sending"); setTimeout(() => { setWarning(""); }, 2000); return; } // add user message locally immediately const userMsg = { id: Date.now() + Math.random(), role: "user", text: input }; setMessages((m) => [...m, userMsg]); setAllChats(prev => ({ ...prev, [sessionId]: [...(prev[sessionId] || []), userMsg] })); const query = input; setInput(""); setLoading(true); setError(null); try { // include recent history so backend can handle follow-ups const recentHistory = messages.slice(-8).map((m) => { return { role: m.role, text: m.text }; }); const form = new FormData(); form.append("session_id", sessionId); form.append("message", query); form.append("top_k", "6"); form.append("history", JSON.stringify(recentHistory)); setChatHistory(prev => { if (prev.some(c => c.id === sessionId)) { return prev; } return [ { id: sessionId, title: query.trim().slice(0, 40) }, ...prev ]; }); const res = await fetch(`${API_BASE}/api/chat`, { method: "POST", body: form, }); if (!res.ok) { const contentType = res.headers.get("content-type") || ""; let text = await res.text(); try { if (contentType.includes("application/json")) { const j = JSON.parse(text); text = j.message || JSON.stringify(j); } } catch (e) { /* ignore */ } throw new Error(text || `HTTP ${res.status}`); } const data = await res.json(); const aiMsg = { id: Date.now() + Math.random(), role: "ai", text: data.answer || data.result || "No answer.", }; setMessages((m) => [...m, aiMsg]); setAllChats(prev => ({ ...prev, [sessionId]: [...(prev[sessionId] || []), aiMsg] })); } catch (err) { console.error(err); setError(err.message || "Chat failed"); addSystemMessage(`Error: ${err.message || "Chat failed"}`); } finally { setLoading(false); } }; const handleKeyDown = (e) => { if (justTranscribedRef.current) { justTranscribedRef.current = false; return; } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunksRef.current, { type: "audio/webm" }); await transcribeAudio(audioBlob); stream.getTracks().forEach((track) => track.stop()); }; mediaRecorder.start(); setIsRecording(true); setError(null); } catch (err) { console.error("Error accessing microphone:", err); setError("Microphone access denied. Please allow microphone permissions."); addSystemMessage("Error: Microphone access denied"); } }; const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); } }; const transcribeAudio = async (audioBlob) => { setIsTranscribing(true); setError(null); try { const form = new FormData(); form.append("audio", audioBlob, "recording.webm"); form.append("session_id", sessionId); const res = await fetch(`${API_BASE}/api/stt`, { method: "POST", body: form, }); if (!res.ok) { const contentType = res.headers.get("content-type") || ""; let text = await res.text(); try { if (contentType.includes("application/json")) { const j = JSON.parse(text); text = j.message || j.error || JSON.stringify(j); } } catch (_) {} throw new Error(text || `HTTP ${res.status}`); } const data = await res.json(); const transcribedText = data.text || data.transcription || data.result || ""; if (transcribedText.trim()) { justTranscribedRef.current = true; setInput((prev) => prev ? `${prev} ${transcribedText}` : transcribedText ); setSttCooldown(true); setTimeout(() => setSttCooldown(false), 500); } } catch (err) { console.error("Transcription error:", err); setError(err.message || "Transcription failed"); addSystemMessage(`Error: ${err.message || "Transcription failed"}`); } finally { setIsTranscribing(false); } }; const toggleRecording = () => { if (isRecording) { stopRecording(); } else { startRecording(); } }; const deleteChat = (id) => { setChatHistory(prev => prev.filter(c => c.id !== id)); setAllChats(prev => { const copy = { ...prev }; delete copy[id]; return copy; }); if (sessionId === id) { setMessages([]); } }; const startNewChat = () => { const newSession = Math.random().toString(36).slice(2, 10); setSessionId(newSession); setMessages([]); setInput(""); setChatHistory(prev => [ { id: newSession, title: "New Chat" }, ...prev ]); localStorage.setItem("rag_session", newSession); }; return (