Aryan
Configure dynamic API_URL in App.js to support relative routing on Hugging Face Spaces
f1c0ad5
Raw
History Blame Contribute Delete
77 kB
import React, { useState, useEffect, useRef } from 'react';
import {
Send, Sparkles, AlertTriangle, CheckCircle, Shield,
Database, Zap, ArrowRight, RefreshCw, Star, Info, Lock, Check, LogOut, Mail, Key, Upload, FileText,
MessageSquare, Trash2, Library, ChevronRight, Activity, FilePlus, Edit2, Copy, User, Eye, EyeOff
} from 'lucide-react';
import './App.css';
const API_URL = process.env.REACT_APP_API_URL || (window.location.hostname === 'localhost' ? 'http://localhost:5000' : '');
function App() {
const [token, setToken] = useState(localStorage.getItem('token') || '');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loginError, setLoginError] = useState('');
// Auth view mode ('login' or 'register')
const [authMode, setAuthMode] = useState('login');
// Registration States
const [regEmail, setRegEmail] = useState('');
const [regPassword, setRegPassword] = useState('');
const [regConfirmPassword, setRegConfirmPassword] = useState('');
const [regName, setRegName] = useState('');
const [regAvatar, setRegAvatar] = useState('👨‍💻');
const [regError, setRegError] = useState('');
// Password Visibility States
const [showLoginPassword, setShowLoginPassword] = useState(false);
const [showRegPassword, setShowRegPassword] = useState(false);
const [showRegConfirmPassword, setShowRegConfirmPassword] = useState(false);
// Navigation Sidebar
const [activeTab, setActiveTab] = useState('workspace'); // 'workspace', 'library', 'telemetry'
// RAG States
const [documents, setDocuments] = useState([]);
const [selectedProjectId, setSelectedProjectId] = useState('');
const [queryAllDocs, setQueryAllDocs] = useState(false);
const [activeTelemetry, setActiveTelemetry] = useState(null);
// Chat Threads States
const [chatThreads, setChatThreads] = useState([]);
const [activeChatId, setActiveChatId] = useState('');
const [messages, setMessages] = useState([]);
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
// User Profile details
const [userStatus, setUserStatus] = useState({
isPremium: false,
name: '',
queryCount: 0,
totalStorageBytes: 0,
uploadedDocuments: []
});
// Library Viewer & File Upload & Copy Paste States
const [selectedDocContent, setSelectedDocContent] = useState(null);
const [viewingDocId, setViewingDocId] = useState('');
const [uploadTitle, setUploadTitle] = useState('');
const [selectedFile, setSelectedFile] = useState(null);
const [copyPasteText, setCopyPasteText] = useState('');
const [ingestMode, setIngestMode] = useState('upload'); // 'upload', 'copypaste'
const [uploadLoading, setUploadLoading] = useState(false);
const [uploadStatus, setUploadStatus] = useState({ type: '', text: '' });
const [copiedId, setCopiedId] = useState('');
const [docSearchQuery, setDocSearchQuery] = useState('');
// Razorpay Simulation Overlay States
const [showRzpModal, setShowRzpModal] = useState(false);
const [rzpOrderDetails, setRzpOrderDetails] = useState(null);
const [rzpPaymentStep, setRzpPaymentStep] = useState('methods'); // 'methods', 'card', 'upi', 'processing'
const [rzpCardNumber, setRzpCardNumber] = useState('');
const [rzpCardExpiry, setRzpCardExpiry] = useState('');
const [rzpCardCvv, setRzpCardCvv] = useState('');
const [rzpUpiId, setRzpUpiId] = useState('');
const [rzpActiveOptions, setRzpActiveOptions] = useState(null);
// Account Settings States
const [profileName, setProfileName] = useState('');
const [profileEmail, setProfileEmail] = useState('');
const [profilePhone, setProfilePhone] = useState('');
const [profilePhoto, setProfilePhoto] = useState('👨‍💻');
const [profileSaveLoading, setProfileSaveLoading] = useState(false);
const [profileSaveStatus, setProfileSaveStatus] = useState({ type: '', text: '' });
const messagesEndRef = useRef(null);
// Initialize and load datasets
useEffect(() => {
if (token) {
fetchUserStatus();
fetchDocuments();
}
}, [token]);
// Synchronize profile states with loaded user details
useEffect(() => {
if (userStatus) {
setProfileName(userStatus.name || '');
setProfileEmail(userStatus.email || '');
setProfilePhone(userStatus.phoneNumber || '');
setProfilePhoto(userStatus.profilePhoto || '👨‍💻');
}
}, [userStatus]);
// Dynamically load Razorpay SDK script
useEffect(() => {
const script = document.createElement("script");
script.src = "https://checkout.razorpay.com/v1/checkout.js";
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
// Load chat threads when project (document) changes
useEffect(() => {
if (token && selectedProjectId) {
fetchChatThreads(selectedProjectId);
} else {
setChatThreads([]);
setActiveChatId('');
setMessages([]);
}
}, [selectedProjectId, token]);
// Load message history when active chat thread changes
useEffect(() => {
if (activeChatId) {
const activeThread = chatThreads.find(t => t.chatId === activeChatId);
if (activeThread) {
setMessages(activeThread.messages || []);
const lastAiMsg = [...(activeThread.messages || [])].reverse().find(m => m.sender === 'ai');
if (lastAiMsg && lastAiMsg.telemetry) {
setActiveTelemetry(lastAiMsg.telemetry);
}
}
} else {
setMessages([]);
}
}, [activeChatId, chatThreads]);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('payment') === 'success') {
alert("🎉 Thank you! Your account has been upgraded to Premium.");
window.history.replaceState({}, document.title, "/");
if (token) fetchUserStatus();
} else if (params.get('payment') === 'cancel') {
alert("❌ Payment was cancelled. Feel free to upgrade anytime!");
window.history.replaceState({}, document.title, "/");
}
}, [token]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleCopyToClipboard = (text, msgIdx) => {
navigator.clipboard.writeText(text);
setCopiedId(msgIdx);
setTimeout(() => setCopiedId(''), 2000);
};
const renderFormattedMessage = (text) => {
if (!text) return null;
const codeBlockRegex = /```([\s\S]*?)```/g;
const parts = [];
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ type: 'text', content: text.substring(lastIndex, match.index) });
}
parts.push({ type: 'code', content: match[1] });
lastIndex = codeBlockRegex.lastIndex;
}
if (lastIndex < text.length) {
parts.push({ type: 'text', content: text.substring(lastIndex) });
}
return parts.map((part, idx) => {
if (part.type === 'code') {
return (
<pre key={idx} className="formatted-code-block">
<code>{part.content.trim()}</code>
</pre>
);
} else {
const lines = part.content.split('\n');
return lines.map((line, lineIdx) => {
const isListItem = line.trim().startsWith('- ') || line.trim().startsWith('* ');
const content = line.replace(/^[\s*-]+/, '');
const formatBold = (str) => {
const boldRegex = /\*\*([\s\S]*?)\*\*/g;
const boldParts = [];
let bLastIndex = 0;
let bMatch;
while ((bMatch = boldRegex.exec(str)) !== null) {
if (bMatch.index > bLastIndex) {
boldParts.push(str.substring(bLastIndex, bMatch.index));
}
boldParts.push(<strong key={bMatch.index}>{bMatch[1]}</strong>);
bLastIndex = boldRegex.lastIndex;
}
if (bLastIndex < str.length) {
boldParts.push(str.substring(bLastIndex));
}
return boldParts.length > 0 ? boldParts : str;
};
if (isListItem) {
return (
<li key={lineIdx} className="formatted-list-item">
{formatBold(content)}
</li>
);
} else {
return (
<p key={lineIdx} className="formatted-paragraph">
{formatBold(line)}
</p>
);
}
});
}
});
};
const handleLogin = async (e, devEmail = null) => {
if (e) e.preventDefault();
setLoginError('');
const targetEmail = devEmail || email;
const targetPassword = devEmail ? 'password123' : password;
try {
const res = await fetch(`${API_URL}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: targetEmail, password: targetPassword })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Login failed");
localStorage.setItem('token', data.token);
setToken(data.token);
setEmail('');
setPassword('');
} catch (err) {
setLoginError(err.message);
}
};
const getPasswordStrength = (pwd) => {
if (!pwd) return { score: 0, label: 'Empty', color: '#718096' };
let score = 0;
if (pwd.length >= 6) score += 1;
if (/\d/.test(pwd)) score += 1;
if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) score += 1;
if (score === 1) return { score: 33, label: 'Weak', color: '#ef4444' };
if (score === 2) return { score: 66, label: 'Medium', color: '#f59e0b' };
if (score === 3) return { score: 100, label: 'Strong', color: '#22c55e' };
return { score: 0, label: 'Empty', color: '#718096' };
};
const handleRegisterSubmit = async (e) => {
e.preventDefault();
setRegError('');
if (regPassword !== regConfirmPassword) {
setRegError("Passwords do not match!");
return;
}
const strength = getPasswordStrength(regPassword);
if (strength.label === 'Weak' || regPassword.length < 6) {
setRegError("Password must be at least 6 characters, contain a number, and a special character.");
return;
}
try {
const res = await fetch(`${API_URL}/api/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: regEmail,
password: regPassword,
name: regName,
profilePhoto: regAvatar
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Registration failed");
localStorage.setItem('token', data.token);
setToken(data.token);
setRegEmail('');
setRegPassword('');
setRegConfirmPassword('');
setRegName('');
setAuthMode('login');
} catch (err) {
setRegError(err.message);
}
};
const handleLogout = () => {
localStorage.removeItem('token');
setToken('');
setUserStatus({ isPremium: false, name: '', queryCount: 0, totalStorageBytes: 0, uploadedDocuments: [] });
setActiveTelemetry(null);
setDocuments([]);
setSelectedProjectId('');
setChatThreads([]);
setActiveChatId('');
setMessages([]);
};
const fetchUserStatus = async () => {
try {
const res = await fetch(`${API_URL}/api/user-status`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (res.status === 401 || res.status === 403) handleLogout();
else setUserStatus(data);
} catch (err) { console.error("Failed to connect to Gateway server."); }
};
const fetchDocuments = async () => {
try {
const res = await fetch(`${API_URL}/api/documents`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (Array.isArray(data)) {
setDocuments(data);
if (data.length > 0 && !selectedProjectId) setSelectedProjectId(data[0].id);
} else {
setDocuments([]);
}
} catch (err) {
console.error("Failed to fetch documents list.");
setDocuments([]);
}
};
const fetchChatThreads = async (projId) => {
try {
const res = await fetch(`${API_URL}/api/projects/${projId}/chats`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
setChatThreads(data);
if (data.length > 0) setActiveChatId(data[0].chatId);
else handleCreateChatThread(projId, "General Discussion");
} catch (err) { console.error("Failed to load chat threads."); }
};
const handleCreateChatThread = async (projId, threadTitle) => {
try {
const res = await fetch(`${API_URL}/api/projects/${projId}/chats`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ title: threadTitle })
});
const newThread = await res.json();
setChatThreads(prev => [...prev, newThread]);
setActiveChatId(newThread.chatId);
} catch (err) { console.error("Failed to create new chat thread."); }
};
const handleDeleteChatThread = async (e, threadId) => {
e.stopPropagation();
try {
await fetch(`${API_URL}/api/projects/${selectedProjectId}/chats/${threadId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
setChatThreads(prev => prev.filter(t => t.chatId !== threadId));
if (activeChatId === threadId) setActiveChatId('');
} catch (err) { console.error("Failed to delete chat thread."); }
};
const handleRenameChatThread = async (e, threadId, currentTitle) => {
e.stopPropagation();
const titleInput = prompt("Rename conversation thread to:", currentTitle);
if (titleInput === null) return;
const newTitle = titleInput.trim() || currentTitle;
if (newTitle === currentTitle) return;
try {
const res = await fetch(`${API_URL}/api/projects/${selectedProjectId}/chats/${threadId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ title: newTitle })
});
const data = await res.json();
if (res.ok) {
setChatThreads(prev => prev.map(t => t.chatId === threadId ? { ...t, chatTitle: data.chatTitle } : t));
}
} catch (err) {
console.error("Failed to rename thread.");
}
};
const handleDeleteDocument = async (e, docId) => {
e.stopPropagation();
if (!window.confirm("Are you sure you want to delete this document from the database? This will permanently wipe its text vectors and conversation history.")) return;
try {
const res = await fetch(`${API_URL}/api/documents/${docId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (res.ok) {
alert("🗑️ Document deleted successfully!");
if (viewingDocId === docId) {
setSelectedDocContent(null);
setViewingDocId('');
}
if (selectedProjectId === docId) {
setSelectedProjectId('');
}
fetchDocuments();
fetchUserStatus();
} else {
alert(`Error: ${data.error || "Failed to delete document"}`);
}
} catch (err) {
alert("Failed to connect to Gateway server during deletion.");
}
};
const handleExportChat = () => {
if (messages.length === 0) {
alert("No messages to export!");
return;
}
let mdContent = `# Chat Export - ${documents.find(d => d.id === selectedProjectId)?.title || "Workspace"}\n`;
mdContent += `Exported on: ${new Date().toLocaleString()}\n\n---\n\n`;
messages.forEach(msg => {
const senderName = msg.sender === 'user' ? 'User' : 'AI Assistant';
mdContent += `### 💬 ${senderName}\n${msg.text}\n\n`;
if (msg.telemetry) {
mdContent += `*Audit Trace: Execution Speed: ${msg.telemetry.execution_time_sec?.toFixed(2)}s | NLI Fact-check: ${msg.telemetry.success ? 'PASSED' : 'BLOCKED'}*\n\n`;
}
mdContent += `---\n\n`;
});
const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `chat_export_${selectedProjectId || 'session'}.md`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleSendQuery = async (e) => {
e.preventDefault();
if (!query.trim() || !activeChatId) return;
const userMessage = { sender: 'user', text: query, timestamp: new Date().toISOString() };
setMessages(prev => [...prev, userMessage]);
setQuery('');
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
query: userMessage.text,
projectId: queryAllDocs ? 'all' : selectedProjectId,
chatId: activeChatId
})
});
const data = await response.json();
if (response.status === 403 && data.is_blocked) {
setMessages(prev => [...prev, { sender: 'ai', text: data.message, isBlocked: true, timestamp: new Date().toISOString() }]);
} else if (data.error) {
setMessages(prev => [...prev, { sender: 'ai', text: `Error: ${data.error}`, timestamp: new Date().toISOString() }]);
} else {
const aiResponse = { sender: 'ai', text: data.answer, telemetry: data.telemetry, timestamp: new Date().toISOString() };
setMessages(prev => [...prev, aiResponse]);
setActiveTelemetry(data.telemetry);
fetchUserStatus();
}
} catch (err) {
setMessages(prev => [...prev, { sender: 'ai', text: "Connection failed.", timestamp: new Date().toISOString() }]);
} finally { setLoading(false); }
};
const handleFileUpload = async (e) => {
e.preventDefault();
if (!selectedFile || !uploadTitle.trim()) return;
setUploadLoading(true);
setUploadStatus({ type: '', text: '' });
const docId = `doc_${uploadTitle.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('doc_id', docId);
formData.append('title', uploadTitle);
try {
const res = await fetch(`${API_URL}/api/ingest-file`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || data.error || "Failed to upload.");
setUploadStatus({ type: 'success', text: `Success: "${uploadTitle}" indexed.` });
setUploadTitle('');
setSelectedFile(null);
const fileInput = document.getElementById('device-file-input');
if (fileInput) fileInput.value = '';
fetchDocuments();
fetchUserStatus();
} catch (err) { setUploadStatus({ type: 'error', text: err.message }); }
finally { setUploadLoading(false); }
};
const handleCopyPasteIngest = async (e) => {
e.preventDefault();
if (!uploadTitle.trim() || !copyPasteText.trim()) return;
setUploadLoading(true);
setUploadStatus({ type: '', text: '' });
const docId = `doc_${uploadTitle.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
try {
const res = await fetch(`${API_URL}/api/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
doc_id: docId,
title: uploadTitle,
text: copyPasteText
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || data.error || "Failed to index text.");
setUploadStatus({ type: 'success', text: `Success: "${uploadTitle}" indexed.` });
setUploadTitle('');
setCopyPasteText('');
fetchDocuments();
fetchUserStatus();
} catch (err) { setUploadStatus({ type: 'error', text: err.message }); }
finally { setUploadLoading(false); }
};
const handleViewDocContent = async (docId) => {
try {
const res = await fetch(`${API_URL}/api/documents/${docId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (data.text) { setSelectedDocContent(data.text); setViewingDocId(docId); }
} catch (err) { alert("Failed to load document text."); }
};
const handleUpgrade = async () => {
try {
const res = await fetch(`${API_URL}/api/create-razorpay-order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
const order = await res.json();
if (res.status !== 200 || !order.id) {
alert("Failed to create payment order: " + (order.error || "Gateway error"));
return;
}
const options = {
key: order.key_id,
amount: order.amount,
currency: order.currency,
name: "VigilantRAG Premium",
description: "Premium Plan Upgrade (Unlock Unlimited Document search, factuality guard)",
order_id: order.id,
handler: async function (response) {
try {
const verifyRes = await fetch(`${API_URL}/api/verify-razorpay-payment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
razorpay_order_id: response.razorpay_order_id,
razorpay_payment_id: response.razorpay_payment_id,
razorpay_signature: response.razorpay_signature,
isMock: order.isMock
})
});
const verifyData = await verifyRes.json();
if (verifyRes.ok) {
alert("🎉 Upgrade successful! Welcome to VigilantRAG Premium.");
fetchUserStatus();
} else {
alert(`Verification failed: ${verifyData.error}`);
}
} catch (err) {
alert("Payment verification connection error.");
}
},
prefill: {
name: userStatus.name || "Aryan",
email: "user@example.com"
},
theme: {
color: "#8b5cf6"
}
};
if (order.isMock) {
console.log("⚠️ Dev Mode: Initializing custom Razorpay overlay simulation.");
setRzpOrderDetails(order);
setRzpActiveOptions(options);
setRzpPaymentStep('methods');
setShowRzpModal(true);
return;
}
if (window.Razorpay) {
const rzp = new window.Razorpay(options);
rzp.open();
} else {
alert("Razorpay payment SDK failed to load. Please check your internet connection.");
}
} catch (err) {
alert("Failed to initiate payment gateway.");
}
};
const handleSaveProfile = async (e) => {
e.preventDefault();
setProfileSaveLoading(true);
setProfileSaveStatus({ type: '', text: '' });
try {
const res = await fetch(`${API_URL}/api/user-profile`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
name: profileName,
email: profileEmail,
phoneNumber: profilePhone,
profilePhoto: profilePhoto
})
});
const data = await res.json();
if (res.ok) {
setProfileSaveStatus({ type: 'success', text: '🎉 Profile changes saved successfully!' });
fetchUserStatus();
} else {
setProfileSaveStatus({ type: 'error', text: data.error || 'Failed to update profile details.' });
}
} catch (err) {
setProfileSaveStatus({ type: 'error', text: 'Gateway server connection failure.' });
} finally {
setProfileSaveLoading(false);
}
};
const handleCancelSubscription = async () => {
const confirmCancel = window.confirm("Are you sure you want to cancel your Premium subscription? \n\nThis will downgrade your account to the Free Plan and lower your document limits.");
if (!confirmCancel) return;
try {
const res = await fetch(`${API_URL}/api/cancel-subscription`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (res.ok) {
alert("🛡️ Subscription cancelled successfully. Account downgraded to Free.");
fetchUserStatus();
} else {
alert("Subscription cancellation request failed.");
}
} catch (err) {
alert("Billing server connection error.");
}
};
if (!token) {
return (
<div className="login-screen-container">
{authMode === 'login' ? (
<div className="login-card">
<div className="login-header">
<Shield className="login-logo" />
<h2>Welcome to VigilantRAG</h2>
<p>Access your self-correcting RAG workspace</p>
</div>
<form onSubmit={(e) => handleLogin(e)} className="login-form">
{loginError && <div className="error-alert">{loginError}</div>}
<div className="input-group">
<Mail className="input-icon" size={18} />
<input type="email" placeholder="Email Address" value={email} onChange={(e) => setEmail(e.target.value)} required />
</div>
<div className="input-group">
<Key className="input-icon" size={18} />
<input
type={showLoginPassword ? "text" : "password"}
className="pwd-input"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button
type="button"
className="password-toggle-btn"
onClick={() => setShowLoginPassword(!showLoginPassword)}
aria-label={showLoginPassword ? "Hide password" : "Show password"}
>
{showLoginPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<button type="submit" className="login-btn">Sign In</button>
</form>
<div className="auth-toggle-link">
<span>Don't have an account? </span>
<button onClick={() => setAuthMode('register')} className="toggle-btn-link">Create new account</button>
</div>
<div className="dev-login-section">
<div className="dev-divider"><span>Sandbox Logins</span></div>
<div className="dev-buttons">
<button onClick={() => handleLogin(null, 'free@example.com')} className="dev-btn free">Free Plan</button>
<button onClick={() => handleLogin(null, 'premium@example.com')} className="dev-btn premium">Premium Plan</button>
</div>
</div>
</div>
) : (
<div className="login-card registration-card">
<div className="login-header">
<Shield className="login-logo" />
<h2>Create Account</h2>
<p>Sign up to start searching and verifying your documents</p>
</div>
<form onSubmit={handleRegisterSubmit} className="login-form">
{regError && <div className="error-alert">{regError}</div>}
<div className="input-group">
<User className="input-icon" size={18} />
<input type="text" placeholder="Full Name" value={regName} onChange={(e) => setRegName(e.target.value)} required />
</div>
<div className="input-group">
<Mail className="input-icon" size={18} />
<input type="email" placeholder="Email Address" value={regEmail} onChange={(e) => setRegEmail(e.target.value)} required />
</div>
<div className="input-group">
<Key className="input-icon" size={18} />
<input
type={showRegPassword ? "text" : "password"}
className="pwd-input"
placeholder="Create Password"
value={regPassword}
onChange={(e) => setRegPassword(e.target.value)}
required
/>
<button
type="button"
className="password-toggle-btn"
onClick={() => setShowRegPassword(!showRegPassword)}
aria-label={showRegPassword ? "Hide password" : "Show password"}
>
{showRegPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{/* Password Strength Meter */}
{regPassword && (
<div className="pwd-strength-container">
<div className="pwd-strength-bar-bg">
<div
className="pwd-strength-bar-fill"
style={{
width: `${getPasswordStrength(regPassword).score}%`,
backgroundColor: getPasswordStrength(regPassword).color
}}
></div>
</div>
<div className="pwd-strength-label" style={{ color: getPasswordStrength(regPassword).color }}>
Strength: <strong>{getPasswordStrength(regPassword).label}</strong>
</div>
</div>
)}
<div className="input-group">
<Check className="input-icon" size={18} />
<input
type={showRegConfirmPassword ? "text" : "password"}
className="pwd-input"
placeholder="Confirm Password"
value={regConfirmPassword}
onChange={(e) => setRegConfirmPassword(e.target.value)}
required
/>
<button
type="button"
className="password-toggle-btn"
onClick={() => setShowRegConfirmPassword(!showRegConfirmPassword)}
aria-label={showRegConfirmPassword ? "Hide password" : "Show password"}
>
{showRegConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{/* Onboarding Avatar Selector */}
<div className="reg-avatar-section">
<p className="avatar-select-label">Select Profile Avatar</p>
<div className="avatar-chips-grid">
{['👨‍💻', '🚀', '🤖', '🕵️‍♂️', '👩‍💻', '🧬', '🧠', '🌟'].map((emoji) => (
<button
key={emoji}
type="button"
className={`avatar-chip-btn ${regAvatar === emoji ? 'selected' : ''}`}
onClick={() => setRegAvatar(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
<button type="submit" className="login-btn" style={{ marginTop: '10px' }}>Create Account</button>
</form>
<div className="auth-toggle-link" style={{ marginBottom: '10px' }}>
<span>Already have an account? </span>
<button onClick={() => setAuthMode('login')} className="toggle-btn-link">Sign In</button>
</div>
</div>
)}
</div>
);
}
return (
<div className="dashboard-container">
<header className="main-header">
<div className="brand">
<Shield className="logo-icon" />
<h1>VigilantRAG <span className="badge">v2.0</span></h1>
</div>
<nav className="header-nav">
<button className={`nav-tab-btn ${activeTab === 'workspace' ? 'active' : ''}`} onClick={() => setActiveTab('workspace')}><MessageSquare size={16} /> Workspace</button>
<button className={`nav-tab-btn ${activeTab === 'library' ? 'active' : ''}`} onClick={() => setActiveTab('library')}><Library size={16} /> Document DB</button>
<button className={`nav-tab-btn ${activeTab === 'telemetry' ? 'active' : ''}`} onClick={() => setActiveTab('telemetry')}><Activity size={16} /> AI Inspector</button>
<button className={`nav-tab-btn ${activeTab === 'account' ? 'active' : ''}`} onClick={() => setActiveTab('account')}><User size={16} /> Account</button>
</nav>
<div className="user-section">
{userStatus.isPremium ? <span className="premium-tag" onClick={() => setActiveTab('account')} style={{ cursor: 'pointer' }}><Star className="icon-star" /> Premium</span> : <button onClick={handleUpgrade} className="upgrade-btn"><Zap className="icon-zap" /> Upgrade</button>}
<button
onClick={() => setActiveTab('account')}
className="header-avatar-btn"
title="Account Settings"
>
{userStatus.profilePhoto || '👨‍💻'}
</button>
<button onClick={handleLogout} className="logout-btn"><LogOut size={16} /></button>
</div>
</header>
<div className="workspace">
{activeTab === 'workspace' && (
<>
<div className="project-threads-sidebar">
<div className="project-list-section">
<div className="sidebar-search-wrapper" style={{ marginBottom: '10px' }}>
<input
type="text"
placeholder="Search projects..."
value={docSearchQuery}
onChange={(e) => setDocSearchQuery(e.target.value)}
className="sidebar-search-input"
style={{
width: '100%',
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'white',
padding: '6px 10px',
fontSize: '0.8rem',
outline: 'none',
boxSizing: 'border-box'
}}
/>
</div>
<h3>Projects</h3>
<div className="project-items">
{documents.filter(doc => doc.title.toLowerCase().includes(docSearchQuery.toLowerCase())).map(doc => (
<button key={doc.id} className={`project-item-btn ${selectedProjectId === doc.id ? 'active' : ''}`} onClick={() => { setSelectedProjectId(doc.id); setQueryAllDocs(false); }}>
<FileText size={14} /> <span className="truncate" style={{ flex: 1 }}>{doc.title}</span>
</button>
))}
{documents.length === 0 && (
<p className="empty-text" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', margin: '10px 0' }}>No projects found.</p>
)}
</div>
</div>
{selectedProjectId && (
<div className="threads-list-section">
<div className="section-header">
<h3>Conversations</h3>
<button
onClick={() => {
const suggested = `Thread #${chatThreads.length + 1}`;
const titleInput = prompt("Enter a name for your new conversation thread:", suggested);
if (titleInput === null) return;
const threadTitle = titleInput.trim() || "New Thread";
handleCreateChatThread(selectedProjectId, threadTitle);
}}
className="new-thread-btn"
title="Add New Thread"
>
<FilePlus size={14} /> New
</button>
</div>
{chatThreads.map(thread => (
<button key={thread.chatId} className={`thread-item-btn ${activeChatId === thread.chatId ? 'active' : ''}`} onClick={() => setActiveChatId(thread.chatId)}>
<span className="truncate">{thread.chatTitle}</span>
<div className="thread-actions" style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<Edit2
size={12}
className="rename-thread-icon"
onClick={(e) => handleRenameChatThread(e, thread.chatId, thread.chatTitle)}
title="Rename Thread"
/>
<Trash2
size={12}
className="delete-thread-icon"
onClick={(e) => handleDeleteChatThread(e, thread.chatId)}
title="Delete Thread"
/>
</div>
</button>
))}
</div>
)}
</div>
<div className="chat-container">
<div className="doc-selector-bar" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{userStatus.isPremium ? (
<label className="checkbox-label">
<input type="checkbox" checked={queryAllDocs} onChange={(e) => setQueryAllDocs(e.target.checked)} />
<span>🔍 Enable Multi-Doc Search (Cross-Document Query)</span>
</label>
) : <span>Single Doc Mode</span>}
{messages.length > 0 && (
<button
onClick={handleExportChat}
className="telemetry-trigger-btn"
style={{ margin: 0, padding: '4px 10px', display: 'flex', alignItems: 'center', gap: '6px' }}
>
📥 Export Chat (.md)
</button>
)}
</div>
<div className="chat-messages">
{messages.length === 0 ? (
<div className="chat-welcome-container">
<Shield className="welcome-logo" size={48} />
<h2>VigilantRAG Active Workspace</h2>
<p>Ask anything about your document project. The AI will search, analyze, and fact-check its answers automatically.</p>
<div className="suggestion-chips-grid">
<button className="suggestion-chip" onClick={() => setQuery("Summarize the key takeaways of this document.")}>
📝 Summarize key takeaways
</button>
<button className="suggestion-chip" onClick={() => setQuery("Are there any security or policy guidelines mentioned?")}>
🔒 Locate policy guidelines
</button>
<button className="suggestion-chip" onClick={() => setQuery("Give me a comprehensive bullet point breakdown.")}>
💡 Get bullet point breakdown
</button>
<button className="suggestion-chip" onClick={() => setQuery("Verify if there are any contradictions in this data.")}>
🛡️ Perform compliance audit
</button>
</div>
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message-bubble ${msg.sender}`}>
<div className="bubble-content">
{msg.sender === 'ai' ? renderFormattedMessage(msg.text) : <p>{msg.text}</p>}
{msg.sender === 'ai' && (
<div className="message-actions-wrapper" style={{ display: 'flex', gap: '8px', marginTop: '10px', alignItems: 'center' }}>
{msg.telemetry && (
<button onClick={() => { setActiveTelemetry(msg.telemetry); setActiveTab('telemetry'); }} className="telemetry-trigger-btn" style={{ margin: 0 }}>
<Activity size={12} /> Inspect AI Logic
</button>
)}
<button onClick={() => handleCopyToClipboard(msg.text, i)} className="copy-message-btn">
{copiedId === i ? <Check size={12} className="text-success" /> : <Copy size={12} />}
<span>{copiedId === i ? "Copied!" : "Copy"}</span>
</button>
</div>
)}
</div>
</div>
))
)}
{loading && (
<div className="message-bubble ai loading">
<div className="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSendQuery} className="chat-input-form">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={activeChatId ? "Type your query here..." : "Select a document project thread to chat..."}
disabled={!activeChatId}
className="chat-input"
/>
<button type="submit" disabled={!activeChatId || loading} className="send-btn">
<Send size={18} />
</button>
</form>
</div>
</>
)}
{activeTab === 'library' && (
<div className="library-container">
<div className="library-sidebar">
<div className="section-header" style={{ marginBottom: '12px' }}>
<h3>Ingested Documents</h3>
<button
onClick={() => {
setSelectedDocContent(null);
setViewingDocId('');
setUploadStatus({ type: '', text: '' });
}}
className="new-thread-btn"
title="Add New Document"
>
<FilePlus size={14} /> Add New
</button>
</div>
<div className="sidebar-search-wrapper" style={{ marginBottom: '16px' }}>
<input
type="text"
placeholder="Search database..."
value={docSearchQuery}
onChange={(e) => setDocSearchQuery(e.target.value)}
className="sidebar-search-input"
style={{
width: '100%',
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'white',
padding: '6px 10px',
fontSize: '0.8rem',
outline: 'none',
boxSizing: 'border-box'
}}
/>
</div>
<div className="library-items" style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{documents.filter(doc => doc.title.toLowerCase().includes(docSearchQuery.toLowerCase())).map(doc => (
<div key={doc.id} className="library-item-wrapper" style={{ display: 'flex', gap: '8px', alignItems: 'center', width: '100%' }}>
<button
className={`library-item-btn ${viewingDocId === doc.id ? 'active' : ''}`}
onClick={() => handleViewDocContent(doc.id)}
style={{ flex: 1 }}
>
<div className="doc-info">
<span className="doc-title">{doc.title}</span>
<span className="doc-meta">
{(doc.text_length / 1024).toFixed(1)} KB | {doc.chunks_count} chunks
</span>
</div>
<ChevronRight size={16} className="item-arrow" />
</button>
<button
onClick={(e) => handleDeleteDocument(e, doc.id)}
className="delete-doc-btn"
title="Delete Document"
style={{
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.2)',
color: 'var(--accent-red)',
padding: '12px',
borderRadius: '8px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Trash2 size={14} />
</button>
</div>
))}
{documents.length === 0 && (
<p className="empty-text" style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>No documents found.</p>
)}
</div>
</div>
<div className="library-viewer-panel">
{selectedDocContent ? (
<div className="doc-text-viewer">
<div className="viewer-header">
<h3>Document Text: <strong>{documents.find(d => d.id === viewingDocId)?.title}</strong></h3>
<button
onClick={() => {
setSelectedDocContent(null);
setViewingDocId('');
setUploadStatus({ type: '', text: '' });
}}
className="close-viewer-btn"
>
← Back & Add Document
</button>
</div>
<div className="viewer-body">
<pre>{selectedDocContent}</pre>
</div>
</div>
) : (
<div className="file-uploader-card">
<div className="upload-header-desc">
<h2>Indexed Document Sandbox</h2>
<p>Index documents directly from your device, or copy-paste raw text contents. They will be partitioned into semantic chunks and embedded instantly.</p>
</div>
<div className="ingest-tabs">
<button
className={`ingest-tab-btn ${ingestMode === 'upload' ? 'active' : ''}`}
onClick={() => setIngestMode('upload')}
>
<Upload size={14} /> Upload Device File
</button>
<button
className={`ingest-tab-btn ${ingestMode === 'copypaste' ? 'active' : ''}`}
onClick={() => setIngestMode('copypaste')}
>
<FileText size={14} /> Copy-Paste Raw Text
</button>
</div>
{uploadStatus.text && (
<div className={`ingest-status-alert ${uploadStatus.type}`}>
{uploadStatus.text}
</div>
)}
{ingestMode === 'upload' ? (
<form onSubmit={handleFileUpload} className="library-upload-form">
<div className="form-group">
<label>Document Title</label>
<input
type="text"
placeholder="e.g. Employee Travel Guidelines"
value={uploadTitle}
onChange={(e) => setUploadTitle(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Select Document File (.pdf, .docx, .xlsx, .txt)</label>
<div className="file-picker-wrapper">
<Upload className="picker-icon" size={28} />
<input
id="device-file-input"
type="file"
accept=".pdf,.docx,.doc,.xlsx,.xls,.txt"
onChange={(e) => {
setSelectedFile(e.target.files[0]);
if (e.target.files[0] && !uploadTitle) {
const nameWithoutExt = e.target.files[0].name.replace(/\.[^/.]+$/, "");
setUploadTitle(nameWithoutExt.replace(/_/g, ' '));
}
}}
required
/>
{selectedFile ? (
<span className="selected-filename">Selected: {selectedFile.name} ({(selectedFile.size / 1024).toFixed(1)} KB)</span>
) : (
<span className="placeholder-filename">Drag & drop your file here, or click to browse files</span>
)}
</div>
</div>
<button type="submit" disabled={uploadLoading} className="submit-upload-btn">
{uploadLoading ? (
<span className="flex-align"><RefreshCw className="animate-spin" size={16} /> Parsing & Indexing...</span>
) : (
<span className="flex-align"><Upload size={16} /> Index Document File</span>
)}
</button>
</form>
) : (
<form onSubmit={handleCopyPasteIngest} className="library-upload-form">
<div className="form-group">
<label>Document Title</label>
<input
type="text"
placeholder="e.g. Leave Policy Addendum"
value={uploadTitle}
onChange={(e) => setUploadTitle(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Paste Document Contents</label>
<textarea
rows={8}
placeholder="Paste raw text content here to index..."
value={copyPasteText}
onChange={(e) => setCopyPasteText(e.target.value)}
required
/>
</div>
<button type="submit" disabled={uploadLoading} className="submit-upload-btn">
{uploadLoading ? (
<span className="flex-align"><RefreshCw className="animate-spin" size={16} /> Embedding Chunks...</span>
) : (
<span className="flex-align"><FileText size={16} /> Index Raw Text Document</span>
)}
</button>
</form>
)}
</div>
)}
</div>
</div>
)}
{activeTab === 'telemetry' && (
<div className="telemetry-dashboard-container">
<div className="telemetry-header-panel">
<Activity className="telemetry-header-icon" />
<div>
<h2>AI Answer Inspector</h2>
<p>Check how the AI analyzed your files and fact-checked its own response to prevent errors.</p>
</div>
</div>
{activeTelemetry ? (
<div className="telemetry-dashboard-grid">
{/* 1. Response Speed Card */}
<div className="telemetry-grid-card border-blue">
<h3>Response Speed</h3>
<div className="big-stat-val text-blue">
{activeTelemetry.execution_time_sec?.toFixed(2) || '0.24'}s
</div>
<span className="badge-friendly badge-blue">⚡ Fast Response</span>
<p className="stat-desc">The time taken by the AI to search your files and construct this answer.</p>
</div>
{/* 2. Documents Scanned Card */}
<div className="telemetry-grid-card border-green">
<h3>Passages Read</h3>
<div className="big-stat-val text-green">
{activeTelemetry.retrieved_attempts?.[0]?.total_candidates || 0}
</div>
<span className="badge-friendly badge-green">🔍 Sources Scanned</span>
<p className="stat-desc">The number of matching paragraphs located in your document library for this search query.</p>
</div>
{/* 3. Accuracy Level Card */}
<div className="telemetry-grid-card border-purple">
<h3>Semantic Match Accuracy</h3>
{activeTelemetry.use_reranking === false ? (
<div className="big-stat-val text-muted flex-align"><Lock size={22} /> Standard</div>
) : (
<div className="big-stat-val text-purple">
{activeTelemetry.retrieved_attempts?.[0]?.top_score ? Math.round(activeTelemetry.retrieved_attempts[0].top_score * 100) : 0}%
</div>
)}
<span className="badge-friendly badge-purple">🎯 Understanding Level</span>
<p className="stat-desc">
{activeTelemetry.use_reranking === false ? (
"Upgrade to Premium to enable advanced semantic semantic re-ranking check."
) : (
`The scanned paragraphs have a ${Math.round(activeTelemetry.retrieved_attempts?.[0]?.top_score * 100)}% match level to the meaning of your question.`
)}
</p>
</div>
{/* 4. Fact-Checking Card */}
<div className="telemetry-grid-card border-orange">
<h3>Factual Verification</h3>
{activeTelemetry.use_nli_guard === false ? (
<div className="big-stat-val text-muted flex-align"><Lock size={22} /> Bypassed</div>
) : (
<div className="big-stat-val text-orange" style={{ color: activeTelemetry.success ? 'var(--accent-green)' : 'var(--accent-red)' }}>
{activeTelemetry.success ? 'PASSED' : 'BLOCKED'}
</div>
)}
<span className="badge-friendly badge-orange">🛡️ Hallucination Audit</span>
<p className="stat-desc">
{activeTelemetry.use_nli_guard === false ? (
"Upgrade to Premium to auto-audit response drafts against raw documents."
) : (
activeTelemetry.success ? "Passed! The AI verified that the answer is 100% supported by the text in your files." : "Audit failed! A potential hallucination was blocked, and the AI was prompted to rewrite it."
)}
</p>
</div>
{/* Safety Check Logs */}
{activeTelemetry.use_nli_guard !== false && activeTelemetry.generation_attempts?.length > 0 && (
<div className="telemetry-grid-full-width">
<h3>Draft Audit History (Step-by-Step AI Logic)</h3>
<div className="nli-scores-logs">
{activeTelemetry.generation_attempts.map((attempt, i) => (
<div key={i} className="nli-log-item">
<span className="log-attempt-badge">Draft Version #{attempt.attempt} (Audited)</span>
<div className="log-probs-wrapper" style={{ marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div className="log-prob-bar-container">
<span className="prob-label">Supported by Documents: <strong>{Math.round(attempt.nli_scores?.entailment * 100)}%</strong></span>
<div className="progress-bar-bg"><div className="progress-bar-fill green" style={{ width: `${attempt.nli_scores?.entailment * 100}%` }}></div></div>
</div>
<div className="log-prob-bar-container">
<span className="prob-label" style={{ color: attempt.nli_scores?.contradiction > 0.15 ? 'var(--accent-red)' : 'var(--text-secondary)' }}>
Contradicts / Made up by AI: <strong>{Math.round(attempt.nli_scores?.contradiction * 100)}%</strong>
</span>
<div className="progress-bar-bg"><div className="progress-bar-fill red" style={{ width: `${attempt.nli_scores?.contradiction * 100}%` }}></div></div>
</div>
</div>
<pre className="log-draft" style={{ marginTop: '12px' }}>Draft Response: "{attempt.response_draft}"</pre>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="telemetry-placeholder-tab">
<RefreshCw className="animate-spin placeholder-icon-tab" />
<h3>No Query Inspected Yet</h3>
<p>Run a search query in your Workspace, then click "Inspect AI Logic" to trace the fact-checking process.</p>
</div>
)}
</div>
)}
{activeTab === 'account' && (
<div className="account-settings-container">
<h2 className="section-title-account">Account Settings</h2>
<div className="account-grid-layout">
{/* Left Column: Profile edit form */}
<div className="account-card profile-info-card">
<h3>Personal Information</h3>
{profileSaveStatus.text && (
<div className={`profile-status-alert ${profileSaveStatus.type}`}>
{profileSaveStatus.text}
</div>
)}
{/* Avatar Picker Section */}
<div className="avatar-picker-section">
<div className="current-avatar-preview">
{profilePhoto}
</div>
<div className="avatar-selection-list">
<p className="avatar-select-label">Choose Avatar Emoji</p>
<div className="avatar-chips-grid">
{['👨‍💻', '🚀', '🤖', '🕵️‍♂️', '👩‍💻', '🧬', '🧠', '🌟'].map((emoji) => (
<button
key={emoji}
type="button"
className={`avatar-chip-btn ${profilePhoto === emoji ? 'selected' : ''}`}
onClick={() => setProfilePhoto(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
</div>
<form onSubmit={handleSaveProfile} className="profile-form">
<div className="form-group-acc">
<label>Full Name</label>
<input
type="text"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
placeholder="e.g. Amit Sharma"
required
/>
</div>
<div className="form-group-acc">
<label>Email Address</label>
<input
type="email"
value={profileEmail}
onChange={(e) => setProfileEmail(e.target.value)}
placeholder="e.g. name@example.com"
required
/>
</div>
<div className="form-group-acc">
<label>Phone Number</label>
<input
type="text"
value={profilePhone}
onChange={(e) => setProfilePhone(e.target.value)}
placeholder="e.g. +91 98765 43210"
/>
</div>
<button type="submit" disabled={profileSaveLoading} className="save-profile-btn">
{profileSaveLoading ? (
<span className="flex-align"><RefreshCw className="animate-spin" size={16} /> Saving...</span>
) : (
'Save Changes'
)}
</button>
</form>
</div>
{/* Right Column: Billing Info & Usage stats */}
<div className="account-card billing-info-card">
<h3>Subscription & Billing</h3>
<div className={`subscription-tier-banner ${userStatus.isPremium ? 'premium' : 'free'}`}>
<div className="tier-header">
<span className="tier-label">{userStatus.isPremium ? 'PREMIUM MEMBER' : 'FREE TIER'}</span>
<span className="tier-badge">{userStatus.isPremium ? '★ Pro' : 'Free'}</span>
</div>
<p className="tier-desc">
{userStatus.isPremium
? 'You have complete access to multi-document search and AI NLI hallucination check guards.'
: 'You are on the basic plan. Upload limits are capped at 3 documents.'
}
</p>
{userStatus.isPremium ? (
<button onClick={handleCancelSubscription} className="cancel-sub-btn">
Cancel Subscription
</button>
) : (
<button onClick={handleUpgrade} className="upgrade-sub-btn">
Upgrade to Premium ($10/mo)
</button>
)}
</div>
{userStatus.isPremium && userStatus.razorpayPaymentId && (
<div className="payment-metadata-box">
<h4>💳 Payment Details</h4>
<div className="metadata-row">
<span className="lbl">Provider</span>
<span className="val">Razorpay Sandbox</span>
</div>
<div className="metadata-row">
<span className="lbl">Payment ID</span>
<span className="val code">{userStatus.razorpayPaymentId}</span>
</div>
<div className="metadata-row">
<span className="lbl">Status</span>
<span className="val green">Success (Active)</span>
</div>
</div>
)}
{/* Storage & Limits */}
<div className="usage-limits-box">
<h3>Limits & Resource Usage</h3>
<div className="usage-progress-item">
<div className="usage-lbl-row">
<span>Documents Uploaded</span>
<strong>{userStatus.uploadedDocuments?.length || 0} / {userStatus.isPremium ? 100 : 3} files</strong>
</div>
<div className="usage-progress-bar-bg">
<div
className="usage-progress-bar-fill fill-blue"
style={{ width: `${Math.min(100, ((userStatus.uploadedDocuments?.length || 0) / (userStatus.isPremium ? 100 : 3)) * 100)}%` }}
></div>
</div>
</div>
<div className="usage-progress-item" style={{ marginTop: '16px' }}>
<div className="usage-lbl-row">
<span>Cloud Storage Used</span>
<strong>{((userStatus.totalStorageBytes || 0) / (1024 * 1024)).toFixed(2)} MB / {userStatus.isPremium ? '5000' : '30'} MB</strong>
</div>
<div className="usage-progress-bar-bg">
<div
className="usage-progress-bar-fill fill-purple"
style={{ width: `${Math.min(100, ((userStatus.totalStorageBytes || 0) / ((userStatus.isPremium ? 5000 : 30) * 1024 * 1024)) * 100)}%` }}
></div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{showRzpModal && rzpOrderDetails && (
<div className="rzp-backdrop">
<div className="rzp-modal-container">
{/* Header */}
<div className="rzp-modal-header">
<div className="rzp-merchant-info">
<div className="rzp-avatar">V</div>
<div>
<h4>VigilantRAG</h4>
<p className="rzp-desc">{rzpActiveOptions?.description || "Premium Plan Upgrade"}</p>
</div>
</div>
<div className="rzp-amount-box">
<span className="rzp-amt">₹{rzpOrderDetails.amount / 100}.00</span>
<span className="rzp-currency">{rzpOrderDetails.currency}</span>
</div>
</div>
{/* Sandbox Alert Banner */}
<div className="rzp-sandbox-banner">
<span>⚠️ TEST MODE (SANDBOX PAYMENTS)</span>
</div>
{/* Body */}
<div className="rzp-modal-body">
{rzpPaymentStep === 'methods' && (
<div className="rzp-methods-list">
<h5>SELECT PAYMENT OPTION</h5>
<button onClick={() => setRzpPaymentStep('card')} className="rzp-method-btn">
<span className="method-emoji">💳</span>
<div className="method-text">
<strong>Pay via Card</strong>
<p>Visa, Mastercard, RuPay, Maestro</p>
</div>
<ChevronRight size={14} className="method-arrow" />
</button>
<button onClick={() => setRzpPaymentStep('upi')} className="rzp-method-btn">
<span className="method-emoji">📱</span>
<div className="method-text">
<strong>UPI / QR Code</strong>
<p>Instant transfer via GPay, PhonePe, Paytm</p>
</div>
<ChevronRight size={14} className="method-arrow" />
</button>
<button
onClick={() => {
setRzpPaymentStep('processing');
setTimeout(() => {
setRzpPaymentStep('success');
setTimeout(() => {
rzpActiveOptions.handler({
razorpay_order_id: rzpOrderDetails.id,
razorpay_payment_id: "pay_mock_" + Date.now(),
razorpay_signature: "mock_dev_signature"
});
setShowRzpModal(false);
}, 1500);
}, 1500);
}}
className="rzp-method-btn"
>
<span>🏛️</span>
<div className="method-text">
<strong>Netbanking</strong>
<p>All major Indian banks available</p>
</div>
<ChevronRight size={14} className="method-arrow" />
</button>
</div>
)}
{rzpPaymentStep === 'card' && (
<form
onSubmit={(e) => {
e.preventDefault();
setRzpPaymentStep('processing');
setTimeout(() => {
setRzpPaymentStep('success');
setTimeout(() => {
rzpActiveOptions.handler({
razorpay_order_id: rzpOrderDetails.id,
razorpay_payment_id: "pay_mock_" + Date.now(),
razorpay_signature: "mock_dev_signature"
});
setShowRzpModal(false);
}, 1500);
}, 1500);
}}
className="rzp-form"
>
{/* Visual Vector Card Preview */}
<div className="vector-card-preview">
<div className="vector-card-header">
<span className="card-logo-visa">VISA</span>
<div className="card-emv-chip"></div>
</div>
<div className="vector-card-number">
{rzpCardNumber || "•••• •••• •••• ••••"}
</div>
<div className="vector-card-footer">
<div>
<p className="card-lbl">HOLDER</p>
<p className="card-val">{rzpActiveOptions?.prefill?.name || "ARYAN DEVELOPER"}</p>
</div>
<div style={{ textAlign: 'right' }}>
<p className="card-lbl">EXPIRES</p>
<p className="card-val">{rzpCardExpiry || "MM/YY"}</p>
</div>
</div>
</div>
<div className="rzp-form-group">
<input
type="text"
placeholder="Card Number"
value={rzpCardNumber}
onChange={(e) => setRzpCardNumber(e.target.value)}
required
/>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
placeholder="MM/YY"
value={rzpCardExpiry}
onChange={(e) => setRzpCardExpiry(e.target.value)}
required
style={{ flex: 1 }}
/>
<input
type="text"
placeholder="CVV"
value={rzpCardCvv}
onChange={(e) => setRzpCardCvv(e.target.value)}
required
style={{ flex: 1 }}
/>
</div>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button type="button" onClick={() => setRzpPaymentStep('methods')} className="rzp-back-btn">Back</button>
<button type="submit" className="rzp-pay-btn">Pay ₹{rzpOrderDetails.amount / 100}</button>
</div>
</form>
)}
{rzpPaymentStep === 'upi' && (
<form
onSubmit={(e) => {
e.preventDefault();
setRzpPaymentStep('processing');
setTimeout(() => {
setRzpPaymentStep('success');
setTimeout(() => {
rzpActiveOptions.handler({
razorpay_order_id: rzpOrderDetails.id,
razorpay_payment_id: "pay_mock_" + Date.now(),
razorpay_signature: "mock_dev_signature"
});
setShowRzpModal(false);
}, 1500);
}, 1500);
}}
className="rzp-form"
>
<div className="upi-logo-box">
<span>UPI</span>
</div>
<div className="rzp-form-group">
<input
type="text"
placeholder="username@upi"
value={rzpUpiId}
onChange={(e) => setRzpUpiId(e.target.value)}
required
/>
</div>
<p className="upi-info-text">You will receive a payment request on your UPI app.</p>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button type="button" onClick={() => setRzpPaymentStep('methods')} className="rzp-back-btn">Back</button>
<button type="submit" className="rzp-pay-btn">Verify & Pay</button>
</div>
</form>
)}
{rzpPaymentStep === 'processing' && (
<div className="rzp-processing-box">
<RefreshCw className="animate-spin text-blue-rzp" size={36} />
<h4>Processing Payment...</h4>
<p>Contacting banking networks securely. Please do not close or go back.</p>
</div>
)}
{rzpPaymentStep === 'success' && (
<div className="rzp-success-payment-box">
<div className="success-checkmark-glow">
<CheckCircle className="success-icon-animated" size={48} />
</div>
<h4>Payment Confirmed</h4>
<p>Upgrading your account to premium plan. Loading workspace...</p>
</div>
)}
</div>
{/* Footer */}
<div className="rzp-modal-footer">
<span className="rzp-lock-icon">🔒</span>
<span>Secured by <strong>Razorpay</strong> test gateway</span>
<button onClick={() => setShowRzpModal(false)} className="rzp-close-btn" title="Cancel Checkout">×</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
export default App;