AiChatbot / src /App.js
Durga-7780's picture
Update src/App.js
07b9b1a verified
Raw
History Blame Contribute Delete
23.4 kB
import React, { useState, useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import FileUploadCard from "./FileUploadCard";
const DeleteBinButton = ({ onClick }) => (
<button className="delete-bin-btn" onClick={onClick} title="Delete chat">
<svg viewBox="0 0 69 14" className="svgIcon bin-top">
<path d="M20.8232 2.62734L19.9948 4.21304C19.8224 4.54309 19.4808 4.75 19.1085 4.75H4.92857C2.20246 4.75 0 6.87266 0 9.5C0 12.1273 2.20246 14.25 4.92857 14.25H64.0714C66.7975 14.25 69 12.1273 69 9.5C69 6.87266 66.7975 4.75 64.0714 4.75H49.8915C49.5192 4.75 49.1776 4.54309 49.0052 4.21305L48.1768 2.62734Z" />
</svg>
<svg viewBox="0 0 69 57" className="svgIcon bin-bottom">
<path d="M64.0023 1.0648C64.0397 0.4882 63.5822 0 63.0044 0H5.99556C5.4178 0 4.96025 0.4882 4.99766 1.0648L8.19375 50.3203C8.44018 54.0758 11.6746 57 15.5712 57H53.4288C57.3254 57 60.5598 54.0758 60.8062 50.3203L64.0023 1.0648Z" />
</svg>
</button>
);
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 (
<div className="app">
{/* ================= LEFT PANEL ================= */}
{/* 🔴 WARNING OVERLAY – PLACE HERE */}
{warning && (
<div
style={{
position: "fixed",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 9999,
background: "rgba(0,0,0,0.35)",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
padding: "14px 18px",
borderRadius: "10px",
border: "2px solid #ff3b3b",
background: "rgba(255,59,59,0.12)",
color: "#ff3b3b",
boxShadow: "0 0 0 1px rgba(255,59,59,.6), 0 12px 30px rgba(255,59,59,.4)",
fontWeight: 600,
}}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
width="26"
height="26"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
{warning}
</div>
</div>
)}
<div className="left">
<button
onClick={startNewChat}
style={{
marginTop: "8px",
marginBottom: "12px",
padding: "10px",
borderRadius: "8px",
background: "rgba(255,255,255,0.08)",
color: "white",
border: "1px solid rgba(255,255,255,0.12)",
cursor: "pointer",
fontWeight: 600
}}
>
New Chat
</button>
<div className="brand">AskMate</div>
{/* -------- Upload Area -------- */}
<div className="upload-area">
<FileUploadCard
files={files}
onFilesChange={setFiles}
uploadProgress={uploadProgress}
uploading={uploading}
onUpload={uploadFiles}
/>
</div>
{/* -------- Chat History -------- */}
<div style={{ marginTop: 16, display: "flex", flexDirection: "column", flex: 1 }}>
<div style={{ fontWeight: 600, marginBottom: 8 }}>
Chat History
</div>
<div className="chat-history-scroll">
{chatHistory.length === 0 && (
<div className="small">No chats yet</div>
)}
{chatHistory.map((chat) => (
<div
key={chat.id}
className="history-item"
style={{
background:
chat.id === sessionId
? "rgba(110,231,183,0.2)"
: "rgba(255,255,255,0.05)",
}}
>
<span
className="history-title"
onClick={() => loadChat(chat.id)}
>
{chat.title}
</span>
<DeleteBinButton onClick={() => deleteChat(chat.id)} />
</div>
))}
</div>
</div>
{/* -------- Chat History -------- */}
<div
style={{
marginTop: "auto",
fontSize: 13,
color: "var(--muted)",
}}
>
Tip: Upload your lecture PDFs first. Then ask focused questions.
</div>
</div>
{/* ================= RIGHT PANEL ================= */}
<div className="right">
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div style={{ fontWeight: 700 }}>Chat</div>
<div className="small">AskMate</div>
</div>
<div className="chat-window">
{messages.map((m) => (
<div
key={m.id}
className={`message-row ${
m.role === "user"
? "user"
: m.role === "ai"
? "ai"
: "system"
}`}
>
<div
className={`bubble ${
m.role === "user" ? "user" : m.role === "ai" ? "ai" : ""
}`}
>
<ReactMarkdown>{m.text}</ReactMarkdown>
{m.role === "system" && (
<div className="meta">{m.text}</div>
)}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div className="input-row">
<textarea
className="text-input"
rows={2}
placeholder="Ask a question (Shift+Enter for new line)"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<input
type="checkbox"
id="mic-toggle"
checked={isRecording}
readOnly
/>
<label
htmlFor="mic-toggle"
className="switch"
onClick={toggleRecording}
title={isRecording ? "Stop recording" : "Start recording"}
style={{ opacity: isTranscribing ? 0.5 : 1, pointerEvents: isTranscribing ? "none" : "auto" }}
>
{/* Mic ON */}
<div className="mic-on">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" />
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z" />
</svg>
</div>
{/* Mic OFF / Recording */}
<div className="mic-off">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1z" />
<path d="M9.486 10.607 5 6.12V8a3 3 0 0 0 4.486 2.607z" />
<path d="M11 3v4.879L5.158 2.037A3.001 3.001 0 0 1 11 3z" />
<path d="M1.646 1.646l12 12 .708-.708-12-12-.708.708z" />
</svg>
</div>
</label>
<button
className="btn"
disabled={loading || isRecording || sttCooldown}
onClick={sendMessage}
>
{loading ? "Thinking..." : "Send"}
</button>
</div>
</div>
</div>
</div>
);
}