Spaces:
Running
Running
Download src/components/AccountManagerModal.tsx from Aura-Studio/AI: direct link, hf CLI and curl.
- Browser
- Download file 155 kB
-
https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/AccountManagerModal.tsx
- Command line
-
hf download hf://spaces/Aura-Studio/AI/src/components/AccountManagerModal.tsx
-
curl -L -o AccountManagerModal.tsx https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/AccountManagerModal.tsx
155 kB
| import React, { useState, useEffect } from "react"; | |
| import { | |
| HardDrive, | |
| Folder, | |
| Image as ImageIcon, | |
| MessageSquare, | |
| Sparkles, | |
| Pin, | |
| Trash2, | |
| Download, | |
| RefreshCw, | |
| AlertTriangle, | |
| CheckCircle2, | |
| X, | |
| User, | |
| LogOut, | |
| FileText, | |
| ExternalLink, | |
| ShieldCheck, | |
| Cloud, | |
| Database, | |
| UploadCloud, | |
| Key, | |
| Code2, | |
| Zap, | |
| Search, | |
| CheckSquare, | |
| Square, | |
| Trash, | |
| AlertOctagon, | |
| Clock, | |
| Info, | |
| CreditCard, | |
| Check, | |
| Crown, | |
| ChevronRight, | |
| Send, | |
| Users, | |
| Sliders, | |
| Award, | |
| Layers, | |
| Copy, | |
| Smartphone, | |
| Settings, | |
| Volume2, | |
| Sun, | |
| Palette, | |
| FolderTree, | |
| FileCode, | |
| Compass, | |
| MapPin, | |
| Globe, | |
| Terminal, | |
| Activity, | |
| CheckCircle, | |
| ToggleLeft, | |
| ToggleRight, | |
| Sparkle, | |
| Cpu, | |
| BookOpen | |
| } from "lucide-react"; | |
| import { TIER_CONFIGS, SubscriptionTier } from "../types/subscription"; | |
| import { AURA_THEMES, AuraTheme } from "../types/theme"; | |
| export interface DataaiFileItem { | |
| name: string; | |
| relativePath: string; | |
| hfStoragePath: string; | |
| sizeBytes: number; | |
| sizeFormatted: string; | |
| updatedAt: string; | |
| category: "users" | "chats" | "media" | "config" | "system"; | |
| isDir: boolean; | |
| } | |
| function formatBytes(bytes: number): string { | |
| if (!bytes || bytes < 0) return "0 B"; | |
| if (bytes < 1024) return `${bytes} B`; | |
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | |
| return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; | |
| } | |
| interface StoredItem { | |
| id: string; | |
| name: string; | |
| folder: "media storage" | "chats" | "generated media" | "pinned chats"; | |
| sizeBytes: number; | |
| sizeFormatted: string; | |
| updatedAt: string; | |
| relativePath: string; | |
| dataUrl?: string; | |
| metadata?: any; | |
| } | |
| interface UpgradeRequest { | |
| id: string; | |
| username: string; | |
| requestedTier: string; | |
| amount: number; | |
| paymentMethod: string; | |
| transactionId: string; | |
| phoneNumber?: string; | |
| note?: string; | |
| status: "pending" | "approved" | "declined"; | |
| createdAt: string; | |
| processedAt?: string; | |
| processedBy?: string; | |
| } | |
| interface UserSummary { | |
| id: string; | |
| username: string; | |
| tier: string; | |
| isAdmin: boolean; | |
| createdAt: string; | |
| lastLoginAt?: string; | |
| usedFormatted: string; | |
| maxFormatted: string; | |
| daysRemaining?: number; | |
| tierExpiresAtFormatted?: string; | |
| hasCustomGptAccess?: boolean; | |
| } | |
| interface AccountManagerModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| currentUser: { | |
| id: string; | |
| username: string; | |
| createdAt?: string; | |
| tier?: string; | |
| token?: string; | |
| stats?: any; | |
| } | null; | |
| onSignOut: () => void; | |
| onRefreshUserStats: () => Promise<void>; | |
| onSyncChats: () => Promise<void>; | |
| onOpenAuraCodex?: () => void; | |
| // Settings & Voice Props | |
| selectedVoiceURI?: string; | |
| onSelectVoice?: (voiceURI: string) => void; | |
| auraThemeId?: string; | |
| onSelectAuraTheme?: (themeId: string) => void; | |
| autoThemeEnabled?: boolean; | |
| onToggleAutoTheme?: (enabled: boolean) => void; | |
| darkMode?: boolean; | |
| onToggleDarkMode?: (dark: boolean) => void; | |
| themeManualOverrideUntil?: number; | |
| initialTab?: "storage" | "dataai_explorer" | "subscriptions" | "my_requests" | "admin_console" | "settings"; | |
| // Accuracy & Google AI Features Props | |
| accuracyMode?: "perfect" | "balanced" | "fast"; | |
| onSelectAccuracyMode?: (mode: "perfect" | "balanced" | "fast") => void; | |
| isOxfordContextActive?: boolean; | |
| onToggleOxfordContext?: (active: boolean) => void; | |
| onOpenOxfordModal?: () => void; | |
| googleSearchGrounding?: boolean; | |
| onToggleGoogleSearchGrounding?: (enabled: boolean) => void; | |
| googleMapsGrounding?: boolean; | |
| onToggleGoogleMapsGrounding?: (enabled: boolean) => void; | |
| fastFlashAccelerator?: boolean; | |
| onToggleFastFlashAccelerator?: (enabled: boolean) => void; | |
| googleCodeExecution?: boolean; | |
| onToggleGoogleCodeExecution?: (enabled: boolean) => void; | |
| firebaseEnabled?: boolean; | |
| onToggleFirebaseEnabled?: (enabled: boolean) => void; | |
| understandingMode?: string; | |
| onSelectUnderstandingMode?: (mode: string) => void; | |
| customApiKey?: string; | |
| onUpdateCustomApiKey?: (key: string) => void; | |
| } | |
| export default function AccountManagerModal({ | |
| isOpen, | |
| onClose, | |
| currentUser, | |
| onSignOut, | |
| onRefreshUserStats, | |
| onSyncChats, | |
| onOpenAuraCodex, | |
| selectedVoiceURI = "", | |
| onSelectVoice, | |
| auraThemeId = "aura-nebula", | |
| onSelectAuraTheme, | |
| autoThemeEnabled = true, | |
| onToggleAutoTheme, | |
| darkMode = true, | |
| onToggleDarkMode, | |
| themeManualOverrideUntil = 0, | |
| initialTab = "subscriptions", | |
| accuracyMode = "perfect", | |
| onSelectAccuracyMode, | |
| isOxfordContextActive = false, | |
| onToggleOxfordContext, | |
| onOpenOxfordModal, | |
| googleSearchGrounding = false, | |
| onToggleGoogleSearchGrounding, | |
| googleMapsGrounding = false, | |
| onToggleGoogleMapsGrounding, | |
| fastFlashAccelerator = true, | |
| onToggleFastFlashAccelerator, | |
| googleCodeExecution = false, | |
| onToggleGoogleCodeExecution, | |
| firebaseEnabled = false, | |
| onToggleFirebaseEnabled, | |
| understandingMode = "ultra-precision", | |
| onSelectUnderstandingMode, | |
| customApiKey = "", | |
| onUpdateCustomApiKey | |
| }: AccountManagerModalProps) { | |
| // Navigation Tabs: storage | dataai_explorer | subscriptions | my_requests | admin_console | settings | |
| const isHarshaAdmin = currentUser?.username?.toLowerCase() === "harsha" || currentUser?.stats?.isAdmin; | |
| const [activeTab, setActiveTab] = useState<"storage" | "dataai_explorer" | "subscriptions" | "my_requests" | "admin_console" | "settings">(initialTab); | |
| const getAuthHeaders = () => { | |
| return currentUser?.token ? { "X-Aura-Auth": currentUser.token } : {}; | |
| }; | |
| useEffect(() => { | |
| if (initialTab) { | |
| setActiveTab(initialTab); | |
| } | |
| }, [initialTab]); | |
| // Speech Synthesis Voices State | |
| const [voices, setVoices] = useState<SpeechSynthesisVoice[]>([]); | |
| useEffect(() => { | |
| if (!("speechSynthesis" in window)) return; | |
| const updateVoices = () => { | |
| const available = window.speechSynthesis.getVoices(); | |
| setVoices(available || []); | |
| }; | |
| updateVoices(); | |
| window.speechSynthesis.onvoiceschanged = updateVoices; | |
| return () => { | |
| if ("speechSynthesis" in window) { | |
| window.speechSynthesis.onvoiceschanged = null; | |
| } | |
| }; | |
| }, []); | |
| const handleTestVoice = (voiceURI?: string) => { | |
| if (!("speechSynthesis" in window)) { | |
| alert("Speech Synthesis is not supported in this browser."); | |
| return; | |
| } | |
| window.speechSynthesis.cancel(); | |
| const testUtterance = new SpeechSynthesisUtterance("Hello! I am Aura AI, speaking with your chosen voice."); | |
| if (voiceURI && voices.length > 0) { | |
| const selected = voices.find(v => v.voiceURI === voiceURI || v.name === voiceURI); | |
| if (selected) testUtterance.voice = selected; | |
| } | |
| testUtterance.rate = 1; | |
| testUtterance.pitch = 1; | |
| window.speechSynthesis.speak(testUtterance); | |
| }; | |
| // Password Change State | |
| const [currentPasswordInput, setCurrentPasswordInput] = useState<string>(""); | |
| const [newPasswordInput, setNewPasswordInput] = useState<string>(""); | |
| const [confirmPasswordInput, setConfirmPasswordInput] = useState<string>(""); | |
| const [passwordStatusMsg, setPasswordStatusMsg] = useState<{ success: boolean; text: string } | null>(null); | |
| const [isSubmittingPassword, setIsSubmittingPassword] = useState<boolean>(false); | |
| const handleChangePasswordSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setPasswordStatusMsg(null); | |
| if (!currentPasswordInput || !newPasswordInput) { | |
| setPasswordStatusMsg({ success: false, text: "Please enter both current and new password." }); | |
| return; | |
| } | |
| if (newPasswordInput !== confirmPasswordInput) { | |
| setPasswordStatusMsg({ success: false, text: "New password and confirmation do not match." }); | |
| return; | |
| } | |
| if (newPasswordInput.length < 6) { | |
| setPasswordStatusMsg({ success: false, text: "New password must be at least 6 characters long." }); | |
| return; | |
| } | |
| setIsSubmittingPassword(true); | |
| try { | |
| const res = await fetch("/api/auth/change-password", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: currentUser?.username, | |
| currentPassword: currentPasswordInput, | |
| newPassword: newPasswordInput | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setPasswordStatusMsg({ success: true, text: data.message || "Password updated successfully!" }); | |
| setCurrentPasswordInput(""); | |
| setNewPasswordInput(""); | |
| setConfirmPasswordInput(""); | |
| } else { | |
| setPasswordStatusMsg({ success: false, text: data.error || "Failed to update password." }); | |
| } | |
| } catch (err: any) { | |
| setPasswordStatusMsg({ success: false, text: err.message || "Network error updating password." }); | |
| } finally { | |
| setIsSubmittingPassword(false); | |
| } | |
| }; | |
| // Storage Tab States | |
| const [activeFolder, setActiveFolder] = useState<"all" | "media storage" | "chats" | "generated media" | "pinned chats">("all"); | |
| const [items, setItems] = useState<StoredItem[]>([]); | |
| const [stats, setStats] = useState<any>(currentUser?.stats || null); | |
| const [loading, setLoading] = useState<boolean>(false); | |
| const [deletingId, setDeletingId] = useState<string | null>(null); | |
| const [bulkDeleting, setBulkDeleting] = useState<boolean>(false); | |
| const [syncing, setSyncing] = useState<boolean>(false); | |
| const [actionMessage, setActionMessage] = useState<string | null>(null); | |
| const [previewItem, setPreviewItem] = useState<StoredItem | null>(null); | |
| // Search & Bulk Selection State | |
| const [searchQuery, setSearchQuery] = useState<string>(""); | |
| const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set()); | |
| // Custom Modal Confirmation Dialogs | |
| const [confirmDeleteModal, setConfirmDeleteModal] = useState<{ itemsToDelete: StoredItem[] } | null>(null); | |
| const [confirmHfModal, setConfirmHfModal] = useState<{ action: "pull" | "push" } | null>(null); | |
| // Hugging Face Persistence State | |
| const [hfStatus, setHfStatus] = useState<any>(null); | |
| const [hfSyncing, setHfSyncing] = useState<boolean>(false); | |
| const [showHfTokenModal, setShowHfTokenModal] = useState<boolean>(false); | |
| const [customHfToken, setCustomHfToken] = useState<string>(""); | |
| const [customHfRepo, setCustomHfRepo] = useState<string>(""); | |
| // DataAI Explorer & Hugging Face Bucket State | |
| const [dataaiFiles, setDataaiFiles] = useState<DataaiFileItem[]>([]); | |
| const [loadingDataai, setLoadingDataai] = useState<boolean>(false); | |
| const [selectedDataaiFile, setSelectedDataaiFile] = useState<DataaiFileItem | null>(null); | |
| const [selectedFileContent, setSelectedFileContent] = useState<string | null>(null); | |
| const [selectedFileContentType, setSelectedFileContentType] = useState<string>("text/plain"); | |
| const [loadingFileContent, setLoadingFileContent] = useState<boolean>(false); | |
| const [dataaiSearch, setDataaiSearch] = useState<string>(""); | |
| const [dataaiFilter, setDataaiFilter] = useState<"all" | "users" | "chats" | "media" | "config">("all"); | |
| const [hfTestLoading, setHfTestLoading] = useState<boolean>(false); | |
| const [hfTestResult, setHfTestResult] = useState<any>(null); | |
| // Subscriptions & Usage Limits State | |
| const [usageData, setUsageData] = useState<any>(null); | |
| const [loadingUsage, setLoadingUsage] = useState<boolean>(false); | |
| const [upgradeModalTier, setUpgradeModalTier] = useState<SubscriptionTier | null>(null); | |
| const [paymentMethodTab, setPaymentMethodTab] = useState<"gpay" | "razorpay">("gpay"); | |
| const [transactionIdInput, setTransactionIdInput] = useState<string>(""); | |
| const [copiedGpay, setCopiedGpay] = useState<boolean>(false); | |
| const [copiedUpiId, setCopiedUpiId] = useState<boolean>(false); | |
| const [isSubmittingTxn, setIsSubmittingTxn] = useState<boolean>(false); | |
| const [paymentSuccessMsg, setPaymentSuccessMsg] = useState<string | null>(null); | |
| // My Requests State | |
| const [myRequests, setMyRequests] = useState<UpgradeRequest[]>([]); | |
| const [loadingMyRequests, setLoadingMyRequests] = useState<boolean>(false); | |
| // Admin Console State (Harsha) | |
| const [adminRequests, setAdminRequests] = useState<UpgradeRequest[]>([]); | |
| const [adminUsers, setAdminUsers] = useState<UserSummary[]>([]); | |
| const [loadingAdminData, setLoadingAdminData] = useState<boolean>(false); | |
| const [adminUserSearch, setAdminUserSearch] = useState<string>(""); | |
| const [changingTierUser, setChangingTierUser] = useState<{ username: string; tier: string } | null>(null); | |
| // Fetch Storage Items | |
| const fetchItems = async () => { | |
| if (!currentUser) return; | |
| setLoading(true); | |
| try { | |
| const res = await fetch(`/api/aura-store/items?username=${encodeURIComponent(currentUser.username)}&token=${encodeURIComponent(currentUser.token || "")}`); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setItems(data.items || []); | |
| if (data.stats) { | |
| setStats(data.stats); | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Failed to load Aura Store items:", e); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // Fetch Usage & Limits | |
| const fetchUsage = async () => { | |
| const uname = currentUser?.username || localStorage.getItem("aura_user_name") || ""; | |
| setLoadingUsage(true); | |
| try { | |
| const res = await fetch(`/api/subscriptions/usage?username=${encodeURIComponent(uname)}`); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setUsageData(data); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load usage data:", e); | |
| } finally { | |
| setLoadingUsage(false); | |
| } | |
| }; | |
| // Fetch User's Own Upgrade Requests | |
| const fetchMyRequests = async () => { | |
| if (!currentUser) return; | |
| setLoadingMyRequests(true); | |
| try { | |
| const res = await fetch(`/api/subscriptions/my-requests?username=${encodeURIComponent(currentUser.username)}`); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setMyRequests(data.requests || []); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load my requests:", e); | |
| } finally { | |
| setLoadingMyRequests(false); | |
| } | |
| }; | |
| // Fetch Admin Requests & Users | |
| const fetchAdminData = async () => { | |
| if (!currentUser || !isHarshaAdmin) return; | |
| setLoadingAdminData(true); | |
| try { | |
| const [reqsRes, usersRes] = await Promise.all([ | |
| fetch(`/api/subscriptions/admin-requests?adminUsername=${encodeURIComponent(currentUser.username)}`), | |
| fetch(`/api/subscriptions/admin-users?adminUsername=${encodeURIComponent(currentUser.username)}`) | |
| ]); | |
| const reqsData = await reqsRes.json(); | |
| const usersData = await usersRes.json(); | |
| if (reqsData.success) setAdminRequests(reqsData.requests || []); | |
| if (usersData.success) setAdminUsers(usersData.users || []); | |
| } catch (e) { | |
| console.error("Failed to load admin data:", e); | |
| } finally { | |
| setLoadingAdminData(false); | |
| } | |
| }; | |
| const fetchHfStatus = async () => { | |
| try { | |
| const res = await fetch("/api/aura-store/hf-sync-status"); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setHfStatus(data); | |
| if (data.repo) setCustomHfRepo(data.repo); | |
| } | |
| } catch (e) {} | |
| }; | |
| const fetchDataaiExplorer = async () => { | |
| setLoadingDataai(true); | |
| try { | |
| const res = await fetch(`/api/aura-store/dataai-explorer?username=${encodeURIComponent(currentUser.username)}&token=${encodeURIComponent(currentUser.token || "")}`); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setDataaiFiles(data.files || []); | |
| if (data.files && data.files.length > 0) { | |
| // If none selected or current selection is not in list, select first | |
| setSelectedDataaiFile(prev => { | |
| const exists = prev ? data.files.some((f: any) => f.relativePath === prev.relativePath) : false; | |
| if (exists) return prev; | |
| handleSelectDataaiFile(data.files[0]); | |
| return data.files[0]; | |
| }); | |
| } else { | |
| setSelectedDataaiFile(null); | |
| setSelectedFileContent(""); | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Failed to load cloud files:", e); | |
| } finally { | |
| setLoadingDataai(false); | |
| } | |
| }; | |
| const handleSelectDataaiFile = async (file: DataaiFileItem) => { | |
| setSelectedDataaiFile(file); | |
| setLoadingFileContent(true); | |
| try { | |
| const res = await fetch(`/api/aura-store/dataai-file?path=${encodeURIComponent(file.relativePath)}&username=${encodeURIComponent(currentUser.username)}&token=${encodeURIComponent(currentUser.token || "")}`); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setSelectedFileContent(data.content); | |
| setSelectedFileContentType(data.contentType || "text/plain"); | |
| } else { | |
| setSelectedFileContent(`Error loading file: ${data.error}`); | |
| } | |
| } catch (e: any) { | |
| setSelectedFileContent(`Failed to read file from cloud storage: ${e.message}`); | |
| } finally { | |
| setLoadingFileContent(false); | |
| } | |
| }; | |
| const handleTestHfConnection = async () => { | |
| setHfTestLoading(true); | |
| setHfTestResult(null); | |
| try { | |
| const res = await fetch("/api/aura-store/hf-test-connection", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", ...getAuthHeaders() }, | |
| body: JSON.stringify({ | |
| repo: customHfRepo.trim(), | |
| token: customHfToken.trim() | |
| }) | |
| }); | |
| const data = await res.json(); | |
| setHfTestResult(data); | |
| } catch (e: any) { | |
| setHfTestResult({ success: false, message: e.message || "Failed to test connection." }); | |
| } finally { | |
| setHfTestLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (isOpen && currentUser) { | |
| fetchItems(); | |
| fetchUsage(); | |
| fetchHfStatus(); | |
| if (activeTab === "dataai_explorer") fetchDataaiExplorer(); | |
| if (activeTab === "my_requests") fetchMyRequests(); | |
| if (activeTab === "admin_console" && isHarshaAdmin) fetchAdminData(); | |
| } | |
| }, [isOpen, currentUser, activeTab]); | |
| if (!isOpen || !currentUser) return null; | |
| // Single Item Delete Handler | |
| const requestDeleteSingle = (item: StoredItem) => { | |
| setConfirmDeleteModal({ itemsToDelete: [item] }); | |
| }; | |
| // Execute Deletion | |
| const executeDelete = async () => { | |
| if (!confirmDeleteModal || confirmDeleteModal.itemsToDelete.length === 0) return; | |
| const targets = confirmDeleteModal.itemsToDelete; | |
| setConfirmDeleteModal(null); | |
| if (targets.length === 1) { | |
| const item = targets[0]; | |
| setDeletingId(item.id); | |
| try { | |
| const res = await fetch("/api/aura-store/delete-item", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", ...getAuthHeaders() }, | |
| body: JSON.stringify({ | |
| username: currentUser.username, | |
| folder: item.folder, | |
| fileName: item.name | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(`Deleted ${item.name}.`); | |
| setItems(prev => prev.filter(i => i.id !== item.id)); | |
| setSelectedItemIds(prev => { | |
| const next = new Set(prev); | |
| next.delete(item.id); | |
| return next; | |
| }); | |
| onRefreshUserStats(); | |
| fetchItems(); | |
| } else { | |
| setActionMessage(data.message || "Failed to delete item."); | |
| } | |
| } catch (e: any) { | |
| setActionMessage(e.message || "Delete request error."); | |
| } finally { | |
| setDeletingId(null); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } | |
| } else { | |
| setBulkDeleting(true); | |
| let successCount = 0; | |
| for (const item of targets) { | |
| try { | |
| const res = await fetch("/api/aura-store/delete-item", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", ...getAuthHeaders() }, | |
| body: JSON.stringify({ | |
| username: currentUser.username, | |
| folder: item.folder, | |
| fileName: item.name | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) successCount++; | |
| } catch (e) {} | |
| } | |
| setActionMessage(`Reclaimed space: Deleted ${successCount} of ${targets.length} files.`); | |
| setBulkDeleting(false); | |
| setSelectedItemIds(new Set()); | |
| onRefreshUserStats(); | |
| fetchItems(); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } | |
| }; | |
| // Hugging Face Pull / Push Handlers | |
| const handleHfPull = async () => { | |
| setConfirmHfModal(null); | |
| setHfSyncing(true); | |
| setActionMessage("Pulling fresh data from Hugging Face bucket..."); | |
| try { | |
| const res = await fetch("/api/aura-store/hf-pull", { | |
| method: "POST", | |
| headers: { ...getAuthHeaders() } | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(`Synced ${data.filesDownloaded} files from Hugging Face bucket.`); | |
| fetchItems(); | |
| fetchHfStatus(); | |
| fetchDataaiExplorer(); | |
| onRefreshUserStats(); | |
| } else { | |
| setActionMessage(data.message || "Pull failed."); | |
| } | |
| } catch (e: any) { | |
| setActionMessage(e.message || "Error pulling from HF."); | |
| } finally { | |
| setHfSyncing(false); | |
| setTimeout(() => setActionMessage(null), 5000); | |
| } | |
| }; | |
| const handleHfPush = async () => { | |
| setConfirmHfModal(null); | |
| setHfSyncing(true); | |
| setActionMessage("Backing up all /dataai files to Hugging Face bucket..."); | |
| try { | |
| const res = await fetch("/api/aura-store/hf-push", { | |
| method: "POST", | |
| headers: { ...getAuthHeaders() } | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(`Backup complete: ${data.filesUploaded} files pushed to HF bucket.`); | |
| fetchHfStatus(); | |
| fetchDataaiExplorer(); | |
| } else { | |
| setActionMessage(data.message || "Push failed."); | |
| } | |
| } catch (e: any) { | |
| setActionMessage(e.message || "Error pushing to HF."); | |
| } finally { | |
| setHfSyncing(false); | |
| setTimeout(() => setActionMessage(null), 5000); | |
| } | |
| }; | |
| const handleSaveHfConfig = async () => { | |
| try { | |
| const res = await fetch("/api/aura-store/hf-config", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", ...getAuthHeaders() }, | |
| body: JSON.stringify({ | |
| repo: customHfRepo.trim(), | |
| token: customHfToken.trim(), | |
| autoSync: true | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setShowHfTokenModal(false); | |
| fetchHfStatus(); | |
| fetchDataaiExplorer(); | |
| setActionMessage("Hugging Face dataset/bucket connection updated."); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } else { | |
| setActionMessage(data.message || "Failed to update HF configuration."); | |
| } | |
| } catch (e: any) { | |
| setActionMessage(e.message || "Failed to update HF configuration."); | |
| } | |
| }; | |
| // Submit UPI Upgrade Transaction | |
| const handleSubmitUpiTransaction = async () => { | |
| if (!upgradeModalTier || !transactionIdInput.trim()) { | |
| alert("Please enter a valid 12-digit UPI Transaction ID or UTR number."); | |
| return; | |
| } | |
| setIsSubmittingTxn(true); | |
| try { | |
| const tierConfig = TIER_CONFIGS[upgradeModalTier]; | |
| const res = await fetch("/api/upi/submit-transaction", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: currentUser.username, | |
| targetTier: upgradeModalTier, | |
| amount: tierConfig.priceINR, | |
| transactionId: transactionIdInput.trim(), | |
| phoneNumber: "9500076449", | |
| note: `Payment for ${tierConfig.name} on Google Pay (9500076449)` | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setPaymentSuccessMsg(`Upgrade request submitted! Txn ID: ${transactionIdInput.trim()}. Admin (Harsha) will verify and activate your tier.`); | |
| setTransactionIdInput(""); | |
| fetchUsage(); | |
| fetchMyRequests(); | |
| setTimeout(() => { | |
| setPaymentSuccessMsg(null); | |
| setUpgradeModalTier(null); | |
| }, 4000); | |
| } else { | |
| alert(data.error || "Failed to submit transaction."); | |
| } | |
| } catch (e: any) { | |
| alert(e.message || "Error submitting transaction."); | |
| } finally { | |
| setIsSubmittingTxn(false); | |
| } | |
| }; | |
| // Razorpay Checkout Handler | |
| const handleRazorpayPayment = async (tierKey: SubscriptionTier) => { | |
| const tierConfig = TIER_CONFIGS[tierKey]; | |
| try { | |
| const orderRes = await fetch("/api/razorpay/create-order", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: currentUser.username, | |
| tier: tierKey, | |
| amount: tierConfig.priceINR * 100 // paise | |
| }) | |
| }); | |
| const orderData = await orderRes.json(); | |
| if (!orderData.success) { | |
| alert(orderData.error || "Failed to create payment order."); | |
| return; | |
| } | |
| // Simulate or trigger Razorpay Checkout | |
| const verifyRes = await fetch("/api/razorpay/verify-payment", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: currentUser.username, | |
| targetTier: tierKey, | |
| order_id: orderData.orderId, | |
| payment_id: `rzp_pay_${Date.now()}` | |
| }) | |
| }); | |
| const verifyData = await verifyRes.json(); | |
| if (verifyData.success) { | |
| setPaymentSuccessMsg(`Instant Upgrade Verified! Your account is now ${tierConfig.name}.`); | |
| onRefreshUserStats(); | |
| fetchUsage(); | |
| setTimeout(() => { | |
| setPaymentSuccessMsg(null); | |
| setUpgradeModalTier(null); | |
| }, 3000); | |
| } | |
| } catch (e: any) { | |
| alert(e.message || "Payment verification failed."); | |
| } | |
| }; | |
| // Admin: Approve Request | |
| const handleAdminApprove = async (requestId: string) => { | |
| try { | |
| const res = await fetch("/api/subscriptions/admin-approve", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| requestId, | |
| adminUsername: currentUser.username | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(data.message || "Request approved and tier updated!"); | |
| fetchAdminData(); | |
| onRefreshUserStats(); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } else { | |
| alert(data.error || "Failed to approve request."); | |
| } | |
| } catch (e: any) { | |
| alert(e.message || "Approval error."); | |
| } | |
| }; | |
| // Admin: Decline Request | |
| const handleAdminDecline = async (requestId: string) => { | |
| try { | |
| const res = await fetch("/api/subscriptions/admin-decline", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| requestId, | |
| adminUsername: currentUser.username | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(data.message || "Request declined."); | |
| fetchAdminData(); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } | |
| } catch (e: any) { | |
| alert(e.message || "Decline error."); | |
| } | |
| }; | |
| // Admin: Directly Set User Tier | |
| const handleAdminSetTier = async (targetUsername: string, newTier: string) => { | |
| try { | |
| const res = await fetch("/api/subscriptions/admin-set-tier", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| targetUsername, | |
| tier: newTier, | |
| days: 30, | |
| adminUsername: currentUser.username | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| setActionMessage(`Updated ${targetUsername} to ${newTier.toUpperCase()} (30 days).`); | |
| setChangingTierUser(null); | |
| fetchAdminData(); | |
| setTimeout(() => setActionMessage(null), 4000); | |
| } | |
| } catch (e: any) { | |
| alert(e.message || "Failed to change user tier."); | |
| } | |
| }; | |
| // Copy helpers | |
| const handleCopyGpayNumber = () => { | |
| navigator.clipboard.writeText("9500076449"); | |
| setCopiedGpay(true); | |
| setTimeout(() => setCopiedGpay(false), 2500); | |
| }; | |
| const handleCopyUpiId = () => { | |
| navigator.clipboard.writeText("9500076449@okaxis"); | |
| setCopiedUpiId(true); | |
| setTimeout(() => setCopiedUpiId(false), 2500); | |
| }; | |
| // Storage Filtering | |
| const filteredItems = items.filter(item => { | |
| const matchesFolder = activeFolder === "all" || item.folder === activeFolder; | |
| const matchesSearch = searchQuery === "" || item.name.toLowerCase().includes(searchQuery.toLowerCase()); | |
| return matchesFolder && matchesSearch; | |
| }); | |
| const filteredDataaiFiles = dataaiFiles.filter(file => { | |
| const matchesSearch = !dataaiSearch.trim() || | |
| file.name.toLowerCase().includes(dataaiSearch.toLowerCase()) || | |
| file.relativePath.toLowerCase().includes(dataaiSearch.toLowerCase()); | |
| if (!matchesSearch) return false; | |
| if (dataaiFilter === "all") return true; | |
| return file.category === dataaiFilter; | |
| }); | |
| const activeTierKey = (currentUser.tier || stats?.tier || "free").toLowerCase(); | |
| const currentTierConfig = TIER_CONFIGS[activeTierKey as SubscriptionTier] || TIER_CONFIGS.free; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-fade-in"> | |
| <div | |
| className="bg-slate-900 border border-slate-700/80 rounded-3xl w-full max-w-5xl max-h-[92vh] flex flex-col shadow-2xl overflow-hidden text-slate-100 relative" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| {/* Top Header */} | |
| <div className="px-6 py-4 border-b border-slate-800 bg-slate-950/80 flex flex-wrap items-center justify-between gap-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-10 w-10 rounded-2xl bg-indigo-500/10 border border-indigo-500/30 flex items-center justify-center text-indigo-400 font-bold"> | |
| <User className="h-5 w-5" /> | |
| </div> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <h3 className="font-bold text-base text-white">@{currentUser.username}</h3> | |
| <span className={`px-2.5 py-0.5 rounded-full text-[11px] font-black uppercase tracking-wider ${ | |
| isHarshaAdmin ? "bg-purple-500/20 text-purple-300 border border-purple-500/40" : | |
| activeTierKey === "auramax" ? "bg-amber-500/20 text-amber-300 border border-amber-500/40" : | |
| activeTierKey === "aurapro" ? "bg-blue-500/20 text-blue-300 border border-blue-500/40" : | |
| activeTierKey === "auraplus" ? "bg-emerald-500/20 text-emerald-300 border border-emerald-500/40" : | |
| "bg-slate-800 text-slate-300 border border-slate-700" | |
| }`}> | |
| {isHarshaAdmin ? "Admin Master" : currentTierConfig.name} | |
| </span> | |
| {stats?.daysRemaining !== undefined && !isHarshaAdmin && activeTierKey !== "free" && ( | |
| <span className="text-[10px] text-amber-400 font-mono bg-amber-500/10 px-2 py-0.5 rounded-md border border-amber-500/20"> | |
| {stats.daysRemaining} days left | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-xs text-slate-400"> | |
| {stats?.usedFormatted || "0 MB"} used of {stats?.maxFormatted || currentTierConfig.storageFormatted} Cloud Storage | |
| </p> | |
| </div> | |
| </div> | |
| {/* Navigation Bar */} | |
| <div className="flex items-center gap-1.5 p-1 bg-slate-900 rounded-2xl border border-slate-800"> | |
| <button | |
| onClick={() => setActiveTab("subscriptions")} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "subscriptions" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200" | |
| }`} | |
| > | |
| <Award className="h-3.5 w-3.5" /> | |
| <span>Plans & Quotas</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("storage")} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "storage" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200" | |
| }`} | |
| > | |
| <HardDrive className="h-3.5 w-3.5" /> | |
| <span>Cloud Storage</span> | |
| </button> | |
| <button | |
| onClick={() => { | |
| setActiveTab("dataai_explorer"); | |
| fetchDataaiExplorer(); | |
| }} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "dataai_explorer" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200" | |
| }`} | |
| title={`View your cloud storage files in /dataai/${currentUser.username}/`} | |
| > | |
| <FolderTree className="h-3.5 w-3.5 text-indigo-400" /> | |
| <span>My Cloud Files</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("my_requests")} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "my_requests" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200" | |
| }`} | |
| > | |
| <CreditCard className="h-3.5 w-3.5" /> | |
| <span>Upgrade Status</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("settings")} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "settings" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200" | |
| }`} | |
| title="Aura Voice, Account Password & Theme Settings" | |
| > | |
| <Settings className="h-3.5 w-3.5" /> | |
| <span>Settings</span> | |
| </button> | |
| {isHarshaAdmin && ( | |
| <button | |
| onClick={() => setActiveTab("admin_console")} | |
| className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition flex items-center gap-1.5 cursor-pointer ${ | |
| activeTab === "admin_console" ? "bg-purple-600 text-white shadow-md" : "text-purple-400 hover:text-purple-200" | |
| }`} | |
| > | |
| <ShieldCheck className="h-3.5 w-3.5" /> | |
| <span>Admin Console</span> | |
| </button> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={onSignOut} | |
| className="px-3 py-1.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 text-xs font-bold flex items-center gap-1.5 cursor-pointer transition" | |
| > | |
| <LogOut className="h-3.5 w-3.5" /> | |
| <span>Sign Out</span> | |
| </button> | |
| <button | |
| onClick={onClose} | |
| className="h-8 w-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center cursor-pointer transition" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Global Notification Banner */} | |
| {actionMessage && ( | |
| <div className="bg-indigo-600/90 text-white text-xs px-6 py-2 flex items-center justify-between font-medium animate-fade-in"> | |
| <span>{actionMessage}</span> | |
| <button onClick={() => setActionMessage(null)} className="text-white/80 hover:text-white"> | |
| <X className="h-3.5 w-3.5" /> | |
| </button> | |
| </div> | |
| )} | |
| {/* TAB 1: SUBSCRIPTIONS & QUOTAS */} | |
| {activeTab === "subscriptions" && ( | |
| <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar"> | |
| {/* Live Daily Quotas Tracker */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center gap-2"> | |
| <Clock className="h-4 w-4 text-indigo-400" /> | |
| <h4 className="text-sm font-bold text-white">Today's Daily Quotas & Usage</h4> | |
| <span className="text-[10px] text-slate-400 font-mono">(Resets daily at 00:00 UTC)</span> | |
| </div> | |
| <button | |
| onClick={fetchUsage} | |
| disabled={loadingUsage} | |
| className="text-xs text-indigo-400 hover:text-indigo-300 flex items-center gap-1 font-semibold" | |
| > | |
| <RefreshCw className={`h-3 w-3 ${loadingUsage ? "animate-spin" : ""}`} /> | |
| <span>Refresh Meters</span> | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3"> | |
| {/* Chat Responses */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><MessageSquare className="h-3 w-3 text-blue-400" /> Chat</span> | |
| </div> | |
| <div className="text-lg font-black text-white"> | |
| {usageData?.usage?.chatResponses || 0} | |
| <span className="text-xs text-slate-400 font-normal"> / {isHarshaAdmin ? "∞" : currentTierConfig.chatResponsesPerDay}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-blue-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, ((usageData?.usage?.chatResponses || 0) / (currentTierConfig.chatResponsesPerDay || 1)) * 100)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* AI Images */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><ImageIcon className="h-3 w-3 text-pink-400" /> Images</span> | |
| </div> | |
| <div className="text-lg font-black text-white"> | |
| {usageData?.usage?.images || 0} | |
| <span className="text-xs text-slate-400 font-normal"> / {isHarshaAdmin ? "∞" : currentTierConfig.imageGenerationsPerDay}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-pink-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, ((usageData?.usage?.images || 0) / (currentTierConfig.imageGenerationsPerDay || 1)) * 100)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* AI Music */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><Sparkles className="h-3 w-3 text-amber-400" /> Music</span> | |
| </div> | |
| <div className="text-lg font-black text-white"> | |
| {usageData?.usage?.music || 0} | |
| <span className="text-xs text-slate-400 font-normal"> / {isHarshaAdmin ? "∞" : currentTierConfig.musicGenerationsPerDay}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-amber-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, ((usageData?.usage?.music || 0) / (currentTierConfig.musicGenerationsPerDay || 1)) * 100)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Documents */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><FileText className="h-3 w-3 text-emerald-400" /> Docs</span> | |
| </div> | |
| <div className="text-lg font-black text-white"> | |
| {usageData?.usage?.documents || 0} | |
| <span className="text-xs text-slate-400 font-normal"> / {isHarshaAdmin ? "∞" : currentTierConfig.documentsPerDay}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-emerald-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, ((usageData?.usage?.documents || 0) / (currentTierConfig.documentsPerDay || 1)) * 100)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Aura Codex */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><Code2 className="h-3 w-3 text-purple-400" /> Codex</span> | |
| </div> | |
| <div className="text-lg font-black text-white"> | |
| {usageData?.usage?.codexMessages || 0} | |
| <span className="text-xs text-slate-400 font-normal"> / {isHarshaAdmin ? "∞" : currentTierConfig.auraCodexPerDay}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-purple-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, ((usageData?.usage?.codexMessages || 0) / (currentTierConfig.auraCodexPerDay || 1)) * 100)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Cloud Storage */} | |
| <div className="bg-slate-900/80 p-3.5 rounded-xl border border-slate-800 flex flex-col justify-between"> | |
| <div className="flex items-center justify-between text-slate-400 text-xs mb-1"> | |
| <span className="flex items-center gap-1"><HardDrive className="h-3 w-3 text-cyan-400" /> Storage</span> | |
| </div> | |
| <div className="text-sm font-black text-white"> | |
| {stats?.usedFormatted || "0 MB"} | |
| <span className="text-[11px] text-slate-400 font-normal"> / {stats?.maxFormatted || currentTierConfig.storageFormatted}</span> | |
| </div> | |
| <div className="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden"> | |
| <div | |
| className="bg-cyan-500 h-full rounded-full" | |
| style={{ width: `${Math.min(100, stats?.percentageUsed || 0)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Subscription Tiers Pricing Grid */} | |
| <div> | |
| <div className="text-center max-w-xl mx-auto mb-6"> | |
| <h3 className="text-xl font-black text-white">Upgrade Your Aura AI Membership</h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Unlock more daily responses, higher cloud storage, fast model fleets, and creative studios. Powered by Google Pay (9500076449) & Razorpay. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> | |
| {/* 1. Free Signed-In */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800 flex flex-col justify-between relative"> | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Free Account</span> | |
| </div> | |
| <div className="text-2xl font-black text-white mb-3"> | |
| ₹0 <span className="text-xs font-normal text-slate-400">/ forever</span> | |
| </div> | |
| <ul className="space-y-2 text-xs text-slate-300"> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> 1.00 GB Cloud Storage</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> 50 Chat responses/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> 15 Image gens/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> 30 Music tracks/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> 50 Codex messages/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> Gemini 2.5, 3.5, 3.6 & GPT-5.4/5.5</li> | |
| </ul> | |
| </div> | |
| <div className="mt-6"> | |
| {activeTierKey === "free" ? ( | |
| <div className="w-full py-2 text-center rounded-xl bg-slate-800 text-slate-300 font-bold text-xs"> | |
| Current Active Plan | |
| </div> | |
| ) : ( | |
| <div className="w-full py-2 text-center rounded-xl bg-slate-900 text-slate-400 text-xs"> | |
| Included Default | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* 2. AuraPlus (₹20/mo) */} | |
| <div className={`bg-slate-950 p-5 rounded-2xl border flex flex-col justify-between relative ${ | |
| activeTierKey === "auraplus" ? "border-emerald-500/60 shadow-emerald-500/10 shadow-xl" : "border-slate-800 hover:border-emerald-500/40" | |
| }`}> | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-emerald-400 uppercase tracking-wider flex items-center gap-1"> | |
| <Zap className="h-3.5 w-3.5" /> AuraPlus | |
| </span> | |
| <span className="px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-300 text-[10px] font-bold border border-emerald-500/30"> | |
| Popular | |
| </span> | |
| </div> | |
| <div className="text-2xl font-black text-white mb-3"> | |
| ₹20 <span className="text-xs font-normal text-slate-400">/ month</span> | |
| </div> | |
| <ul className="space-y-2 text-xs text-slate-300"> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> <strong>2.00 GB</strong> Cloud Storage</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> <strong>70</strong> Chat responses/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> <strong>20</strong> Image gens/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> <strong>50</strong> Music tracks/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> <strong>100</strong> Codex messages/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> All Gemini Models + GPT-5.6 Luna</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-emerald-400 shrink-0" /> Auto-tune to index.html view</li> | |
| </ul> | |
| </div> | |
| <div className="mt-6"> | |
| {activeTierKey === "auraplus" ? ( | |
| <div className="w-full py-2 text-center rounded-xl bg-emerald-600 text-white font-bold text-xs shadow-md"> | |
| Active Plan | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setUpgradeModalTier("auraplus")} | |
| className="w-full py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs cursor-pointer shadow-md transition" | |
| > | |
| Upgrade (₹20/mo) | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* 3. AuraPro (₹50/mo) */} | |
| <div className={`bg-slate-950 p-5 rounded-2xl border flex flex-col justify-between relative ${ | |
| activeTierKey === "aurapro" ? "border-blue-500/60 shadow-blue-500/10 shadow-xl" : "border-slate-800 hover:border-blue-500/40" | |
| }`}> | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-blue-400 uppercase tracking-wider flex items-center gap-1"> | |
| <Sparkles className="h-3.5 w-3.5" /> AuraPro | |
| </span> | |
| </div> | |
| <div className="text-2xl font-black text-white mb-3"> | |
| ₹50 <span className="text-xs font-normal text-slate-400">/ month</span> | |
| </div> | |
| <ul className="space-y-2 text-xs text-slate-300"> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> <strong>3.00 GB</strong> Cloud Storage</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> <strong>100</strong> Chat responses/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> <strong>30</strong> Image gens/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> <strong>60</strong> Music tracks/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> <strong>200</strong> Codex messages/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> GPT-5.6 Terra & Qwen 2.5 Coder</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-blue-400 shrink-0" /> Computer-Use Terminal Studio</li> | |
| </ul> | |
| </div> | |
| <div className="mt-6"> | |
| {activeTierKey === "aurapro" ? ( | |
| <div className="w-full py-2 text-center rounded-xl bg-blue-600 text-white font-bold text-xs shadow-md"> | |
| Active Plan | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setUpgradeModalTier("aurapro")} | |
| className="w-full py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-bold text-xs cursor-pointer shadow-md transition" | |
| > | |
| Upgrade (₹50/mo) | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* 4. AuraMax (₹100/mo) */} | |
| <div className={`bg-slate-950 p-5 rounded-2xl border flex flex-col justify-between relative ${ | |
| activeTierKey === "auramax" ? "border-amber-500/60 shadow-amber-500/10 shadow-xl" : "border-slate-800 hover:border-amber-500/40" | |
| }`}> | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-amber-400 uppercase tracking-wider flex items-center gap-1"> | |
| <Crown className="h-3.5 w-3.5" /> AuraMax | |
| </span> | |
| <span className="px-2 py-0.5 rounded-full bg-amber-500/10 text-amber-300 text-[10px] font-bold border border-amber-500/30"> | |
| Ultimate | |
| </span> | |
| </div> | |
| <div className="text-2xl font-black text-white mb-3"> | |
| ₹100 <span className="text-xs font-normal text-slate-400">/ month</span> | |
| </div> | |
| <ul className="space-y-2 text-xs text-slate-300"> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> <strong>5.00 GB</strong> Cloud Storage</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> <strong>200</strong> Chat responses/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> <strong>50</strong> Image gens/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> <strong>100</strong> Music tracks/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> <strong>300</strong> Codex messages/day</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> GPT-5.6 Sol + Full Model Fleet</li> | |
| <li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 shrink-0" /> Custom GPT Creator & Store</li> | |
| </ul> | |
| </div> | |
| <div className="mt-6"> | |
| {activeTierKey === "auramax" ? ( | |
| <div className="w-full py-2 text-center rounded-xl bg-amber-600 text-white font-bold text-xs shadow-md"> | |
| Active Plan | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setUpgradeModalTier("auramax")} | |
| className="w-full py-2 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs cursor-pointer shadow-md transition" | |
| > | |
| Upgrade (₹100/mo) | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 2: CLOUD STORAGE & FILE MANAGER */} | |
| {activeTab === "storage" && ( | |
| <div className="flex-1 flex flex-col md:flex-row overflow-hidden"> | |
| {/* Sidebar Navigation */} | |
| <div className="w-full md:w-64 border-b md:border-b-0 md:border-r border-slate-800 p-4 space-y-4 bg-slate-950/60 shrink-0"> | |
| <div className="space-y-1"> | |
| <span className="text-[10px] font-bold uppercase tracking-wider text-slate-400 px-3">Folders (/dataai)</span> | |
| <button | |
| onClick={() => { setActiveFolder("all"); setSelectedItemIds(new Set()); }} | |
| className={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${ | |
| activeFolder === "all" ? "bg-indigo-600 text-white shadow-sm" : "text-slate-300 hover:bg-slate-800/60" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Folder className="h-4 w-4" /> | |
| <span>All User Files</span> | |
| </div> | |
| <span className="text-[10px] bg-slate-800/80 px-2 py-0.5 rounded-full text-slate-300 font-mono"> | |
| {items.length} | |
| </span> | |
| </button> | |
| <button | |
| onClick={() => { setActiveFolder("media storage"); setSelectedItemIds(new Set()); }} | |
| className={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${ | |
| activeFolder === "media storage" ? "bg-indigo-600 text-white shadow-sm" : "text-slate-300 hover:bg-slate-800/60" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <HardDrive className="h-4 w-4 text-emerald-400" /> | |
| <span>media storage</span> | |
| </div> | |
| <span className="text-[10px] bg-slate-800/80 px-2 py-0.5 rounded-full text-slate-300 font-mono"> | |
| {items.filter(i => i.folder === "media storage").length} | |
| </span> | |
| </button> | |
| <button | |
| onClick={() => { setActiveFolder("chats"); setSelectedItemIds(new Set()); }} | |
| className={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${ | |
| activeFolder === "chats" ? "bg-indigo-600 text-white shadow-sm" : "text-slate-300 hover:bg-slate-800/60" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <MessageSquare className="h-4 w-4 text-blue-400" /> | |
| <span>chats</span> | |
| </div> | |
| <span className="text-[10px] bg-slate-800/80 px-2 py-0.5 rounded-full text-slate-300 font-mono"> | |
| {items.filter(i => i.folder === "chats").length} | |
| </span> | |
| </button> | |
| <button | |
| onClick={() => { setActiveFolder("generated media"); setSelectedItemIds(new Set()); }} | |
| className={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${ | |
| activeFolder === "generated media" ? "bg-indigo-600 text-white shadow-sm" : "text-slate-300 hover:bg-slate-800/60" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Sparkles className="h-4 w-4 text-amber-400" /> | |
| <span>generated media</span> | |
| </div> | |
| <span className="text-[10px] bg-slate-800/80 px-2 py-0.5 rounded-full text-slate-300 font-mono"> | |
| {items.filter(i => i.folder === "generated media").length} | |
| </span> | |
| </button> | |
| <button | |
| onClick={() => { setActiveFolder("pinned chats"); setSelectedItemIds(new Set()); }} | |
| className={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${ | |
| activeFolder === "pinned chats" ? "bg-indigo-600 text-white shadow-sm" : "text-slate-300 hover:bg-slate-800/60" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Pin className="h-4 w-4 text-purple-400" /> | |
| <span>pinned chats</span> | |
| </div> | |
| <span className="text-[10px] bg-slate-800/80 px-2 py-0.5 rounded-full text-slate-300 font-mono"> | |
| {items.filter(i => i.folder === "pinned chats").length} | |
| </span> | |
| </button> | |
| </div> | |
| {/* User Private Storage Volume Banner */} | |
| <div className="p-3 bg-slate-900 rounded-2xl border border-slate-800 space-y-2 text-xs"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-1.5 font-bold text-white text-[11px]"> | |
| <HardDrive className="h-3.5 w-3.5 text-indigo-400" /> | |
| <span>My Cloud Volume</span> | |
| </div> | |
| <span className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-emerald-500/10 text-emerald-300 border border-emerald-500/30"> | |
| Private | |
| </span> | |
| </div> | |
| <p className="text-[10px] text-slate-400 leading-tight font-mono truncate"> | |
| /dataai/{currentUser.username}/ | |
| </p> | |
| <div className="flex flex-col gap-1.5 pt-1"> | |
| <button | |
| onClick={() => { | |
| setActiveTab("dataai_explorer"); | |
| fetchDataaiExplorer(); | |
| }} | |
| className="w-full py-1.5 px-2 rounded-lg bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-300 font-semibold text-[10px] flex items-center justify-center gap-1.5 cursor-pointer border border-indigo-500/30 transition" | |
| title={`Explore files stored in /dataai/${currentUser.username}/`} | |
| > | |
| <FolderTree className="h-3.5 w-3.5 text-indigo-400" /> | |
| <span>Open My Cloud Explorer</span> | |
| </button> | |
| {isHarshaAdmin && ( | |
| <div className="flex gap-1 pt-1 border-t border-slate-800"> | |
| <button | |
| onClick={() => setConfirmHfModal({ action: "push" })} | |
| disabled={hfSyncing} | |
| className="flex-1 py-1 px-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-[9px] flex items-center justify-center gap-1 cursor-pointer" | |
| title="Admin: Push all files to HF Bucket" | |
| > | |
| <UploadCloud className="h-2.5 w-2.5 text-emerald-400" /> | |
| <span>HF Push</span> | |
| </button> | |
| <button | |
| onClick={() => setConfirmHfModal({ action: "pull" })} | |
| disabled={hfSyncing} | |
| className="flex-1 py-1 px-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-[9px] flex items-center justify-center gap-1 cursor-pointer" | |
| title="Admin: Pull files from HF Bucket" | |
| > | |
| <Download className="h-2.5 w-2.5 text-amber-400" /> | |
| <span>HF Pull</span> | |
| </button> | |
| <button | |
| onClick={() => { | |
| setCustomHfRepo(hfStatus?.repo || ""); | |
| setHfTestResult(null); | |
| setShowHfTokenModal(true); | |
| }} | |
| className="py-1 px-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-400 font-semibold text-[9px] flex items-center justify-center cursor-pointer transition border border-slate-700" | |
| title="Configure Master Bucket" | |
| > | |
| <Settings className="h-2.5 w-2.5" /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Main File Browser & Controls */} | |
| <div className="flex-1 flex flex-col overflow-hidden bg-slate-900/40"> | |
| {/* Search & Actions Bar */} | |
| <div className="p-4 border-b border-slate-800 flex flex-wrap items-center justify-between gap-3 bg-slate-950/40"> | |
| <div className="flex items-center gap-2 flex-1 min-w-[200px]"> | |
| <div className="relative w-full max-w-sm"> | |
| <Search className="h-3.5 w-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" /> | |
| <input | |
| type="text" | |
| placeholder="Search files or chats..." | |
| value={searchQuery} | |
| onChange={e => setSearchQuery(e.target.value)} | |
| className="w-full pl-8 pr-3 py-1.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white placeholder:text-slate-400 focus:outline-none focus:border-indigo-500" | |
| /> | |
| </div> | |
| <button | |
| onClick={fetchItems} | |
| disabled={loading} | |
| className="p-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 text-xs cursor-pointer" | |
| title="Refresh List" | |
| > | |
| <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} /> | |
| </button> | |
| </div> | |
| {/* Bulk Actions */} | |
| {selectedItemIds.size > 0 && ( | |
| <div className="flex items-center gap-2 bg-indigo-950/60 border border-indigo-500/30 px-3 py-1.5 rounded-xl animate-fade-in"> | |
| <span className="text-xs text-indigo-300 font-bold">{selectedItemIds.size} selected</span> | |
| <button | |
| onClick={() => { | |
| const targets = items.filter(i => selectedItemIds.has(i.id)); | |
| setConfirmDeleteModal({ itemsToDelete: targets }); | |
| }} | |
| className="px-2.5 py-1 rounded-lg bg-red-600 hover:bg-red-500 text-white font-bold text-xs flex items-center gap-1 cursor-pointer" | |
| > | |
| <Trash2 className="h-3 w-3" /> | |
| <span>Delete Selected</span> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Items Table / List */} | |
| <div className="flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar"> | |
| {loading ? ( | |
| <div className="py-20 flex flex-col items-center justify-center text-slate-400 gap-2"> | |
| <RefreshCw className="h-6 w-6 animate-spin text-indigo-400" /> | |
| <span className="text-xs">Reading /dataai storage...</span> | |
| </div> | |
| ) : filteredItems.length === 0 ? ( | |
| <div className="py-20 flex flex-col items-center justify-center text-slate-400 gap-2 text-center"> | |
| <Folder className="h-8 w-8 text-slate-600" /> | |
| <p className="text-sm font-bold text-slate-300">No items found in {activeFolder}</p> | |
| <p className="text-xs text-slate-400">Chats, images, and audio will appear here automatically.</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-1.5"> | |
| {filteredItems.map(item => { | |
| const isSelected = selectedItemIds.has(item.id); | |
| return ( | |
| <div | |
| key={item.id} | |
| className={`p-3 rounded-2xl border transition flex items-center justify-between gap-3 ${ | |
| isSelected ? "bg-indigo-950/40 border-indigo-500/50" : "bg-slate-950/70 border-slate-800/80 hover:border-slate-700" | |
| }`} | |
| > | |
| <div className="flex items-center gap-3 min-w-0 flex-1"> | |
| <button | |
| onClick={() => { | |
| setSelectedItemIds(prev => { | |
| const next = new Set(prev); | |
| if (next.has(item.id)) next.delete(item.id); | |
| else next.add(item.id); | |
| return next; | |
| }); | |
| }} | |
| className="text-slate-400 hover:text-indigo-400 cursor-pointer" | |
| > | |
| {isSelected ? <CheckSquare className="h-4 w-4 text-indigo-400" /> : <Square className="h-4 w-4" />} | |
| </button> | |
| <div className="h-8 w-8 rounded-xl bg-slate-900 border border-slate-800 flex items-center justify-center shrink-0"> | |
| {item.folder === "chats" || item.folder === "pinned chats" ? ( | |
| <MessageSquare className="h-4 w-4 text-blue-400" /> | |
| ) : ( | |
| <ImageIcon className="h-4 w-4 text-amber-400" /> | |
| )} | |
| </div> | |
| <div className="min-w-0 flex-1"> | |
| <p className="text-xs font-bold text-white truncate">{item.name}</p> | |
| <div className="flex items-center gap-2 text-[10px] text-slate-400 font-mono mt-0.5"> | |
| <span className="bg-slate-900 px-1.5 py-0.5 rounded text-slate-300">{item.folder}</span> | |
| <span>{item.sizeFormatted}</span> | |
| <span>• {new Date(item.updatedAt).toLocaleDateString()}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-1.5"> | |
| {item.dataUrl && ( | |
| <button | |
| onClick={() => setPreviewItem(item)} | |
| className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs cursor-pointer" | |
| title="Preview" | |
| > | |
| <ExternalLink className="h-3.5 w-3.5" /> | |
| </button> | |
| )} | |
| <button | |
| onClick={() => requestDeleteSingle(item)} | |
| disabled={deletingId === item.id} | |
| className="p-1.5 rounded-lg bg-red-500/10 hover:bg-red-500/20 text-red-400 text-xs cursor-pointer" | |
| title="Delete Item" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB: MY CLOUD FILES (/dataai/{username}/) */} | |
| {activeTab === "dataai_explorer" && ( | |
| <div className="flex-1 flex flex-col overflow-hidden bg-slate-950/60"> | |
| {/* Top Summary & Control Ribbon */} | |
| <div className="p-4 border-b border-slate-800 bg-slate-900/80 flex flex-wrap items-center justify-between gap-4"> | |
| <div className="flex items-center gap-3 min-w-[280px]"> | |
| <div className="h-10 w-10 rounded-2xl bg-indigo-500/10 border border-indigo-500/30 flex items-center justify-center text-indigo-400 font-bold shrink-0"> | |
| <FolderTree className="h-5 w-5" /> | |
| </div> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <h4 className="font-bold text-sm text-white"> | |
| {isHarshaAdmin ? "Master Cloud Directory (/dataai)" : `@${currentUser.username}'s Cloud Storage`} | |
| </h4> | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-300 border border-emerald-500/30"> | |
| {isHarshaAdmin ? "Admin Master" : "User Isolated • Private"} | |
| </span> | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-500/10 text-indigo-300 border border-indigo-500/30"> | |
| Persistent Cloud | |
| </span> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-slate-400 mt-0.5"> | |
| <span className="font-mono text-slate-300"> | |
| Path: <strong className="text-emerald-400">/dataai/{isHarshaAdmin ? "" : `${currentUser.username}/`}</strong> | |
| </span> | |
| <span>•</span> | |
| <span>{dataaiFiles.length} private files ({formatBytes(dataaiFiles.reduce((acc, f) => acc + (f.sizeBytes || 0), 0))})</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {isHarshaAdmin && ( | |
| <> | |
| <button | |
| onClick={() => setConfirmHfModal({ action: "push" })} | |
| disabled={hfSyncing} | |
| className="px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs flex items-center gap-1.5 cursor-pointer shadow-sm transition disabled:opacity-50" | |
| title="Admin: Upload all files from /dataai to Hugging Face Bucket" | |
| > | |
| <UploadCloud className="h-3.5 w-3.5" /> | |
| <span>Backup All</span> | |
| </button> | |
| <button | |
| onClick={() => setConfirmHfModal({ action: "pull" })} | |
| disabled={hfSyncing} | |
| className="px-3 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs flex items-center gap-1.5 cursor-pointer shadow-sm transition disabled:opacity-50" | |
| title="Admin: Download and restore files from Hugging Face Bucket" | |
| > | |
| <Download className="h-3.5 w-3.5" /> | |
| <span>Restore All</span> | |
| </button> | |
| <button | |
| onClick={() => { | |
| setCustomHfRepo(hfStatus?.repo || ""); | |
| setHfTestResult(null); | |
| setShowHfTokenModal(true); | |
| }} | |
| className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 font-bold text-xs flex items-center gap-1.5 cursor-pointer transition" | |
| title="Configure Master Bucket" | |
| > | |
| <Settings className="h-3.5 w-3.5 text-amber-400" /> | |
| <span>Bucket Config</span> | |
| </button> | |
| </> | |
| )} | |
| <button | |
| onClick={fetchDataaiExplorer} | |
| disabled={loadingDataai} | |
| className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 text-xs font-semibold flex items-center gap-1.5 cursor-pointer" | |
| title="Refresh my cloud files" | |
| > | |
| <RefreshCw className={`h-3.5 w-3.5 ${loadingDataai ? "animate-spin" : ""}`} /> | |
| <span>Refresh</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Explorer Split Panes */} | |
| <div className="flex-1 flex flex-col md:flex-row overflow-hidden"> | |
| {/* Left Pane: File Directory */} | |
| <div className="w-full md:w-80 lg:w-96 border-b md:border-b-0 md:border-r border-slate-800 flex flex-col bg-slate-950/50 shrink-0"> | |
| {/* Search & Filter Toolbar */} | |
| <div className="p-3 border-b border-slate-800 space-y-2"> | |
| <div className="relative"> | |
| <Search className="h-3.5 w-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" /> | |
| <input | |
| type="text" | |
| placeholder={`Search in @${currentUser.username}...`} | |
| value={dataaiSearch} | |
| onChange={e => setDataaiSearch(e.target.value)} | |
| className="w-full pl-8 pr-3 py-1.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500" | |
| /> | |
| </div> | |
| <div className="flex items-center gap-1 overflow-x-auto pb-1 text-[11px] custom-scrollbar"> | |
| {(["all", "chats", "media", "users", "config"] as const).map(cat => ( | |
| <button | |
| key={cat} | |
| onClick={() => setDataaiFilter(cat)} | |
| className={`px-2.5 py-1 rounded-lg capitalize font-semibold transition cursor-pointer whitespace-nowrap ${ | |
| dataaiFilter === cat ? "bg-indigo-600 text-white shadow-sm" : "bg-slate-900 text-slate-400 hover:text-slate-200 border border-slate-800" | |
| }`} | |
| > | |
| {cat === "users" ? "userData" : cat} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Files List */} | |
| <div className="flex-1 overflow-y-auto p-2 space-y-1 custom-scrollbar"> | |
| {loadingDataai ? ( | |
| <div className="py-12 flex flex-col items-center justify-center text-slate-400 text-xs gap-2"> | |
| <RefreshCw className="h-5 w-5 animate-spin text-indigo-400" /> | |
| <span>Reading @{currentUser.username}'s cloud storage...</span> | |
| </div> | |
| ) : filteredDataaiFiles.length === 0 ? ( | |
| <div className="py-12 text-center text-slate-500 text-xs px-4"> | |
| <Folder className="h-8 w-8 mx-auto mb-2 text-slate-600" /> | |
| <p>No files found in your cloud storage matching "{dataaiSearch}".</p> | |
| <p className="text-[10px] text-slate-600 mt-1">Start chatting or saving media to populate your account folder.</p> | |
| </div> | |
| ) : ( | |
| filteredDataaiFiles.map(file => { | |
| const isSelected = selectedDataaiFile?.relativePath === file.relativePath; | |
| return ( | |
| <button | |
| key={file.relativePath} | |
| onClick={() => handleSelectDataaiFile(file)} | |
| className={`w-full text-left p-2.5 rounded-xl border transition cursor-pointer flex items-start gap-2.5 ${ | |
| isSelected | |
| ? "bg-indigo-600/15 border-indigo-500/50 text-white shadow-sm" | |
| : "bg-slate-900/60 border-slate-800/80 text-slate-300 hover:bg-slate-900 hover:border-slate-700" | |
| }`} | |
| > | |
| <div className="mt-0.5 shrink-0"> | |
| {file.category === "users" ? ( | |
| <User className="h-4 w-4 text-emerald-400" /> | |
| ) : file.category === "chats" ? ( | |
| <MessageSquare className="h-4 w-4 text-indigo-400" /> | |
| ) : file.category === "media" ? ( | |
| <ImageIcon className="h-4 w-4 text-pink-400" /> | |
| ) : file.category === "config" ? ( | |
| <Settings className="h-4 w-4 text-cyan-400" /> | |
| ) : ( | |
| <FileText className="h-4 w-4 text-slate-400" /> | |
| )} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between gap-1"> | |
| <span className="font-semibold text-xs text-white truncate">{file.name}</span> | |
| <span className="text-[10px] font-mono text-slate-400 shrink-0">{file.sizeFormatted}</span> | |
| </div> | |
| <div className="flex items-center justify-between gap-1 text-[10px] text-slate-400 font-mono mt-0.5"> | |
| <span className="truncate text-slate-400">{file.relativePath}</span> | |
| <span className="shrink-0 text-slate-500">{new Date(file.updatedAt).toLocaleDateString()}</span> | |
| </div> | |
| </div> | |
| </button> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| {/* Right Pane: File Viewer & Inspector */} | |
| <div className="flex-1 flex flex-col overflow-hidden bg-slate-950/80"> | |
| {selectedDataaiFile ? ( | |
| <> | |
| {/* Viewer Header */} | |
| <div className="p-3.5 border-b border-slate-800 bg-slate-900/70 flex flex-wrap items-center justify-between gap-3"> | |
| <div className="flex items-center gap-2.5 min-w-0"> | |
| <div className="p-1.5 rounded-lg bg-slate-800 text-indigo-400 shrink-0"> | |
| <FileCode className="h-4 w-4" /> | |
| </div> | |
| <div className="min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <h5 className="font-bold text-xs text-white font-mono truncate">/dataai/{selectedDataaiFile.relativePath}</h5> | |
| <span className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-emerald-500/10 text-emerald-300 border border-emerald-500/30 shrink-0"> | |
| Private File | |
| </span> | |
| </div> | |
| <p className="text-[11px] text-slate-400 font-mono"> | |
| Size: {selectedDataaiFile.sizeFormatted} • Last Modified: {new Date(selectedDataaiFile.updatedAt).toLocaleString()} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={() => { | |
| if (selectedFileContent) { | |
| navigator.clipboard.writeText(selectedFileContent); | |
| setActionMessage("Copied file content to clipboard."); | |
| setTimeout(() => setActionMessage(null), 3000); | |
| } | |
| }} | |
| className="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-xs flex items-center gap-1 cursor-pointer transition border border-slate-700" | |
| title="Copy file content" | |
| > | |
| <Copy className="h-3 w-3" /> | |
| <span>Copy</span> | |
| </button> | |
| <button | |
| onClick={() => { | |
| if (selectedFileContent) { | |
| const blob = new Blob([selectedFileContent], { type: selectedFileContentType }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = selectedDataaiFile.name; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| }} | |
| className="px-2.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-xs flex items-center gap-1 cursor-pointer transition border border-slate-700" | |
| title="Download file to device" | |
| > | |
| <Download className="h-3 w-3" /> | |
| <span>Download</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Viewer Content */} | |
| <div className="flex-1 overflow-auto p-4 font-mono text-xs text-slate-200 bg-slate-950 custom-scrollbar"> | |
| {loadingFileContent ? ( | |
| <div className="py-20 flex flex-col items-center justify-center text-slate-400 gap-2"> | |
| <RefreshCw className="h-5 w-5 animate-spin text-indigo-400" /> | |
| <span>Reading /dataai/{selectedDataaiFile.relativePath}...</span> | |
| </div> | |
| ) : selectedFileContentType.startsWith("image/") ? ( | |
| <div className="flex flex-col items-center justify-center p-6 space-y-4"> | |
| <img | |
| src={`data:${selectedFileContentType};base64,${selectedFileContent}`} | |
| alt={selectedDataaiFile.name} | |
| className="max-h-96 max-w-full rounded-xl object-contain border border-slate-800 shadow-lg" | |
| /> | |
| <span className="text-xs text-slate-400">{selectedDataaiFile.name} ({selectedDataaiFile.sizeFormatted})</span> | |
| </div> | |
| ) : selectedFileContentType.startsWith("audio/") ? ( | |
| <div className="flex flex-col items-center justify-center p-8 space-y-4"> | |
| <Volume2 className="h-12 w-12 text-purple-400" /> | |
| <audio | |
| controls | |
| src={`data:${selectedFileContentType};base64,${selectedFileContent}`} | |
| className="w-full max-w-md" | |
| /> | |
| <span className="text-xs text-slate-400">{selectedDataaiFile.name}</span> | |
| </div> | |
| ) : ( | |
| <pre className="whitespace-pre-wrap break-all leading-relaxed text-slate-300 font-mono text-[11px] bg-slate-900/60 p-4 rounded-xl border border-slate-800/80"> | |
| {selectedFileContent} | |
| </pre> | |
| )} | |
| </div> | |
| </> | |
| ) : ( | |
| <div className="flex-1 flex flex-col items-center justify-center p-8 text-center text-slate-500 space-y-3"> | |
| <div className="h-14 w-14 rounded-2xl bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-600"> | |
| <FolderTree className="h-7 w-7 text-indigo-400" /> | |
| </div> | |
| <h5 className="font-bold text-sm text-slate-300">Select a file from your cloud folder</h5> | |
| <p className="text-xs text-slate-500 max-w-sm"> | |
| Click any file on the left to inspect its contents, size, and download or copy securely from your isolated storage. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 3: MY UPGRADE REQUESTS */} | |
| {activeTab === "my_requests" && ( | |
| <div className="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h4 className="text-base font-bold text-white">My Subscription Upgrade Requests</h4> | |
| <p className="text-xs text-slate-400">Track payment status and manual approvals from Admin (Harsha).</p> | |
| </div> | |
| <button | |
| onClick={fetchMyRequests} | |
| disabled={loadingMyRequests} | |
| className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold flex items-center gap-1.5" | |
| > | |
| <RefreshCw className={`h-3 w-3 ${loadingMyRequests ? "animate-spin" : ""}`} /> | |
| <span>Refresh Status</span> | |
| </button> | |
| </div> | |
| {loadingMyRequests ? ( | |
| <div className="py-20 flex justify-center text-slate-400"> | |
| <RefreshCw className="h-6 w-6 animate-spin text-indigo-400" /> | |
| </div> | |
| ) : myRequests.length === 0 ? ( | |
| <div className="py-16 text-center bg-slate-950 rounded-2xl border border-slate-800 p-6"> | |
| <CreditCard className="h-10 w-10 text-slate-600 mx-auto mb-2" /> | |
| <p className="text-sm font-bold text-slate-300">No Upgrade Requests Yet</p> | |
| <p className="text-xs text-slate-400 mt-1">Upgrade to AuraPlus (₹20), AuraPro (₹50), or AuraMax (₹100) from the Plans tab.</p> | |
| <button | |
| onClick={() => setActiveTab("subscriptions")} | |
| className="mt-4 px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs" | |
| > | |
| View Plans | |
| </button> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {myRequests.map(req => ( | |
| <div key={req.id} className="p-4 rounded-2xl bg-slate-950 border border-slate-800 flex items-center justify-between gap-4"> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-bold text-white text-sm uppercase">{req.requestedTier}</span> | |
| <span className="text-xs font-black text-indigo-400 font-mono">₹{req.amount}</span> | |
| <span className={`px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider ${ | |
| req.status === "approved" ? "bg-emerald-500/20 text-emerald-300 border border-emerald-500/40" : | |
| req.status === "declined" ? "bg-red-500/20 text-red-300 border border-red-500/40" : | |
| "bg-amber-500/20 text-amber-300 border border-amber-500/40" | |
| }`}> | |
| {req.status === "approved" ? "Approved & Activated" : req.status === "declined" ? "Declined" : "Pending Admin Review"} | |
| </span> | |
| </div> | |
| <div className="text-xs text-slate-400 font-mono mt-1 space-x-3"> | |
| <span>Txn: {req.transactionId}</span> | |
| <span>• Via: {req.paymentMethod === "google_pay" ? "Google Pay (9500076449)" : "Razorpay"}</span> | |
| <span>• Date: {new Date(req.createdAt).toLocaleString()}</span> | |
| </div> | |
| {req.note && <p className="text-xs text-slate-300 mt-1 italic">"{req.note}"</p>} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* TAB 4: ADMIN CONSOLE (HARSHA ONLY) */} | |
| {activeTab === "admin_console" && isHarshaAdmin && ( | |
| <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar"> | |
| <div className="flex items-center justify-between border-b border-purple-500/30 pb-3"> | |
| <div className="flex items-center gap-2"> | |
| <ShieldCheck className="h-5 w-5 text-purple-400" /> | |
| <h4 className="text-base font-bold text-white">Harsha Master Admin Control Console</h4> | |
| </div> | |
| <button | |
| onClick={fetchAdminData} | |
| disabled={loadingAdminData} | |
| className="px-3 py-1.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white text-xs font-bold flex items-center gap-1.5 cursor-pointer shadow-md" | |
| > | |
| <RefreshCw className={`h-3 w-3 ${loadingAdminData ? "animate-spin" : ""}`} /> | |
| <span>Refresh Console</span> | |
| </button> | |
| </div> | |
| {/* Section A: Pending Upgrade Requests */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800 space-y-3"> | |
| <h5 className="text-sm font-bold text-amber-400 flex items-center gap-2"> | |
| <Clock className="h-4 w-4" /> Pending Payment Approvals ({adminRequests.filter(r => r.status === "pending").length}) | |
| </h5> | |
| {adminRequests.filter(r => r.status === "pending").length === 0 ? ( | |
| <p className="text-xs text-slate-400 py-3">No pending upgrade requests. All user payments processed!</p> | |
| ) : ( | |
| <div className="space-y-2.5"> | |
| {adminRequests.filter(r => r.status === "pending").map(req => ( | |
| <div key={req.id} className="p-3.5 rounded-xl bg-slate-900 border border-slate-800 flex flex-wrap items-center justify-between gap-3"> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <strong className="text-white text-sm">@{req.username}</strong> | |
| <span className="px-2 py-0.5 rounded-md bg-indigo-500/20 text-indigo-300 font-bold text-xs"> | |
| Upgrade to {req.requestedTier.toUpperCase()} | |
| </span> | |
| <span className="font-bold text-emerald-400 text-xs font-mono">₹{req.amount}</span> | |
| </div> | |
| <div className="text-xs text-slate-400 font-mono mt-1 space-x-2"> | |
| <span>Txn / UTR: <strong className="text-amber-300">{req.transactionId}</strong></span> | |
| <span>• Phone: {req.phoneNumber || "9500076449"}</span> | |
| <span>• {new Date(req.createdAt).toLocaleString()}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={() => handleAdminApprove(req.id)} | |
| className="px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs flex items-center gap-1 cursor-pointer shadow-sm" | |
| > | |
| <CheckCircle2 className="h-3.5 w-3.5" /> | |
| <span>Approve & Activate</span> | |
| </button> | |
| <button | |
| onClick={() => handleAdminDecline(req.id)} | |
| className="px-3 py-1.5 rounded-xl bg-red-600/80 hover:bg-red-600 text-white font-bold text-xs cursor-pointer" | |
| > | |
| Decline | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| {/* Section B: Manage All Registered Users */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800 space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <h5 className="text-sm font-bold text-purple-400 flex items-center gap-2"> | |
| <Users className="h-4 w-4" /> Registered Users & Tiers ({adminUsers.length}) | |
| </h5> | |
| <input | |
| type="text" | |
| placeholder="Filter users..." | |
| value={adminUserSearch} | |
| onChange={e => setAdminUserSearch(e.target.value)} | |
| className="px-3 py-1 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white placeholder:text-slate-400" | |
| /> | |
| </div> | |
| <div className="space-y-2 max-h-[300px] overflow-y-auto custom-scrollbar"> | |
| {adminUsers | |
| .filter(u => adminUserSearch === "" || u.username.toLowerCase().includes(adminUserSearch.toLowerCase())) | |
| .map(u => ( | |
| <div key={u.id} className="p-3 rounded-xl bg-slate-900/90 border border-slate-800 flex items-center justify-between gap-3 text-xs"> | |
| <div> | |
| <div className="flex items-center gap-2 font-bold text-white"> | |
| <span>@{u.username}</span> | |
| <span className="px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 font-mono text-[10px] uppercase"> | |
| {u.tier} | |
| </span> | |
| {u.daysRemaining !== undefined && ( | |
| <span className="text-[10px] text-amber-400 font-mono">({u.daysRemaining} days left)</span> | |
| )} | |
| </div> | |
| <p className="text-slate-400 font-mono text-[10px] mt-0.5"> | |
| Storage: {u.usedFormatted} / {u.maxFormatted} • Joined: {new Date(u.createdAt).toLocaleDateString()} | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <select | |
| defaultValue={u.tier} | |
| onChange={e => handleAdminSetTier(u.username, e.target.value)} | |
| className="bg-slate-950 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white focus:outline-none" | |
| > | |
| <option value="free">Free</option> | |
| <option value="auraplus">AuraPlus (₹20)</option> | |
| <option value="aurapro">AuraPro (₹50)</option> | |
| <option value="auramax">AuraMax (₹100)</option> | |
| <option value="admin">Admin</option> | |
| </select> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 5: SETTINGS */} | |
| {activeTab === "settings" && ( | |
| <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar"> | |
| {/* Header Banner */} | |
| <div className="bg-gradient-to-r from-indigo-950/80 via-slate-900 to-purple-950/80 p-5 rounded-2xl border border-indigo-500/20 shadow-xl flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2"> | |
| <div className="h-7 w-7 rounded-lg bg-indigo-600 flex items-center justify-center text-white font-bold"> | |
| <Settings className="h-4 w-4" /> | |
| </div> | |
| <h3 className="text-base font-bold text-white">System, AI & Engine Settings</h3> | |
| </div> | |
| <p className="text-xs text-slate-300"> | |
| Configure cognitive accuracy, automatic day/night theme scheduling, voice synthesis, and connected Google AI features. | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-[10px] font-mono font-bold px-2.5 py-1 rounded-lg bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 flex items-center gap-1.5"> | |
| <Activity className="h-3 w-3 text-emerald-400" /> | |
| <span>Sub-Second Flash Active</span> | |
| </span> | |
| </div> | |
| </div> | |
| {/* Section 1: Accuracy Engine [99.999%] */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <ShieldCheck className="h-4 w-4 text-emerald-400" /> | |
| <h4 className="text-sm font-bold text-white">Accuracy Engine</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-300 border border-emerald-500/20 font-bold"> | |
| {accuracyMode === "perfect" ? "99.999% Factual Precision" : accuracyMode === "fast" ? "Fast Burst" : "Creative Balanced"} | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Controls factual rigor, hallucination suppression, and deterministic verification across all chat responses. | |
| </p> | |
| <div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (onSelectAccuracyMode) onSelectAccuracyMode("perfect"); | |
| if (onSelectUnderstandingMode) onSelectUnderstandingMode("ultra-precision"); | |
| }} | |
| className={`p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between ${ | |
| accuracyMode === "perfect" | |
| ? "bg-emerald-950/40 border-emerald-500/80 ring-2 ring-emerald-500/30 text-white shadow-lg" | |
| : "bg-slate-900/60 border-slate-800 hover:border-slate-700 text-slate-300" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-emerald-300 flex items-center gap-1.5"> | |
| <CheckCircle className="h-3.5 w-3.5" /> 99.999% Perfect Standard | |
| </span> | |
| {accuracyMode === "perfect" && <span className="h-2 w-2 rounded-full bg-emerald-400 animate-ping" />} | |
| </div> | |
| <p className="text-[11px] text-slate-400 leading-relaxed mb-2"> | |
| Zero fluff, deterministic 0.15 temperature, peer-reviewed accuracy, and exhaustive factual detail. | |
| </p> | |
| <span className="text-[9.5px] font-mono font-bold text-emerald-400 uppercase"> | |
| {accuracyMode === "perfect" ? "Active Standard" : "Select Standard"} | |
| </span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (onSelectAccuracyMode) onSelectAccuracyMode("balanced"); | |
| if (onSelectUnderstandingMode) onSelectUnderstandingMode("grade-7"); | |
| }} | |
| className={`p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between ${ | |
| accuracyMode === "balanced" | |
| ? "bg-indigo-950/40 border-indigo-500/80 ring-2 ring-indigo-500/30 text-white shadow-lg" | |
| : "bg-slate-900/60 border-slate-800 hover:border-slate-700 text-slate-300" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-indigo-300 flex items-center gap-1.5"> | |
| <Sparkles className="h-3.5 w-3.5" /> Balanced Creative | |
| </span> | |
| {accuracyMode === "balanced" && <span className="h-2 w-2 rounded-full bg-indigo-400" />} | |
| </div> | |
| <p className="text-[11px] text-slate-400 leading-relaxed mb-2"> | |
| Balanced temperature (0.4) for natural conversational flow, brainstorming, and writing assistance. | |
| </p> | |
| <span className="text-[9.5px] font-mono font-bold text-indigo-400 uppercase"> | |
| {accuracyMode === "balanced" ? "Active Standard" : "Select Standard"} | |
| </span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (onSelectAccuracyMode) onSelectAccuracyMode("fast"); | |
| if (onSelectUnderstandingMode) onSelectUnderstandingMode("grade-4"); | |
| }} | |
| className={`p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between ${ | |
| accuracyMode === "fast" | |
| ? "bg-amber-950/40 border-amber-500/80 ring-2 ring-amber-500/30 text-white shadow-lg" | |
| : "bg-slate-900/60 border-slate-800 hover:border-slate-700 text-slate-300" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs font-bold text-amber-300 flex items-center gap-1.5"> | |
| <Zap className="h-3.5 w-3.5" /> Ultra-Fast Flash | |
| </span> | |
| {accuracyMode === "fast" && <span className="h-2 w-2 rounded-full bg-amber-400" />} | |
| </div> | |
| <p className="text-[11px] text-slate-400 leading-relaxed mb-2"> | |
| High velocity sub-second responses optimized for rapid Q&A and instant summarization. | |
| </p> | |
| <span className="text-[9.5px] font-mono font-bold text-amber-400 uppercase"> | |
| {accuracyMode === "fast" ? "Active Standard" : "Select Standard"} | |
| </span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Section 2: Added Google AI Features */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Cpu className="h-4 w-4 text-cyan-400" /> | |
| <h4 className="text-sm font-bold text-white">Google AI Connected Features & Tools</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-cyan-500/10 text-cyan-300 border border-cyan-500/20 font-bold"> | |
| Gemini Grounding & Tools | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Connect official Google intelligence engines into Aura Chat for real-time verification, geospatial intelligence, code execution, and flash streaming. | |
| </p> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-3.5"> | |
| {/* Google Maps Data & Grounding */} | |
| <div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-emerald-500/10 text-emerald-400 flex items-center justify-center shrink-0 mt-0.5"> | |
| <MapPin className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white flex items-center gap-1.5"> | |
| <span>Google Maps Geospatial Data</span> | |
| <span className="text-[9px] px-1.5 py-0.2 rounded bg-emerald-500/20 text-emerald-300 font-mono font-bold">LIVE</span> | |
| </div> | |
| <div className="text-[11px] text-slate-400 mt-0.5"> | |
| Grounds answers with Google Maps places, addresses, routing directions, traffic, and points of interest. | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => onToggleGoogleMapsGrounding && onToggleGoogleMapsGrounding(!googleMapsGrounding)} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| googleMapsGrounding ? "bg-emerald-600 text-white shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {googleMapsGrounding ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| {/* Google Search Real-Time Grounding */} | |
| <div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-blue-500/10 text-blue-400 flex items-center justify-center shrink-0 mt-0.5"> | |
| <Globe className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white flex items-center gap-1.5"> | |
| <span>Google Search Live Grounding</span> | |
| <span className="text-[9px] px-1.5 py-0.2 rounded bg-blue-500/20 text-blue-300 font-mono font-bold">2026 WEB</span> | |
| </div> | |
| <div className="text-[11px] text-slate-400 mt-0.5"> | |
| Fetches up-to-the-minute web information, current news, and verified factual citations. | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => onToggleGoogleSearchGrounding && onToggleGoogleSearchGrounding(!googleSearchGrounding)} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| googleSearchGrounding ? "bg-blue-600 text-white shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {googleSearchGrounding ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| {/* Google Fast-Flash Token Accelerator */} | |
| <div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-amber-500/10 text-amber-400 flex items-center justify-center shrink-0 mt-0.5"> | |
| <Zap className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white flex items-center gap-1.5"> | |
| <span>Google Fast-Flash Accelerator</span> | |
| <span className="text-[9px] px-1.5 py-0.2 rounded bg-amber-500/20 text-amber-300 font-mono font-bold">1-SEC BURST</span> | |
| </div> | |
| <div className="text-[11px] text-slate-400 mt-0.5"> | |
| Delivers instant streaming with zero artificial buffering for a flash of a second response speed. | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => onToggleFastFlashAccelerator && onToggleFastFlashAccelerator(!fastFlashAccelerator)} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| fastFlashAccelerator ? "bg-amber-500 text-slate-950 font-black shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {fastFlashAccelerator ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| {/* Google Code Sandbox & Python Execution */} | |
| <div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-purple-500/10 text-purple-400 flex items-center justify-center shrink-0 mt-0.5"> | |
| <Terminal className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white flex items-center gap-1.5"> | |
| <span>Google Code Execution Sandbox</span> | |
| <span className="text-[9px] px-1.5 py-0.2 rounded bg-purple-500/20 text-purple-300 font-mono font-bold">PYTHON</span> | |
| </div> | |
| <div className="text-[11px] text-slate-400 mt-0.5"> | |
| Executes live code in a sandboxed Python runtime for mathematical verification and computation. | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => onToggleGoogleCodeExecution && onToggleGoogleCodeExecution(!googleCodeExecution)} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| googleCodeExecution ? "bg-purple-600 text-white shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {googleCodeExecution ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| {/* Firebase / Aura Store Persistent Mode */} | |
| <div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-orange-500/10 text-orange-400 flex items-center justify-center shrink-0 mt-0.5"> | |
| <Database className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white flex items-center gap-1.5"> | |
| <span>Firebase Persistence Mode</span> | |
| <span className="text-[9px] px-1.5 py-0.2 rounded bg-orange-500/20 text-orange-300 font-mono font-bold">STORE</span> | |
| </div> | |
| <div className="text-[11px] text-slate-400 mt-0.5"> | |
| Enables deep integration with Aura Store / Firebase for persistent cloud file management and historical session retrieval. | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => onToggleFirebaseEnabled && onToggleFirebaseEnabled(!firebaseEnabled)} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| firebaseEnabled ? "bg-orange-600 text-white shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {firebaseEnabled ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Section 2.5: Oxford English Academic Knowledge Base */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <BookOpen className="h-4 w-4 text-amber-400" /> | |
| <h4 className="text-sm font-bold text-white">Oxford Knowledge Base</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-amber-500/10 text-amber-300 border border-amber-500/20 font-bold"> | |
| {isOxfordContextActive ? "100% LEXICAL ACTIVE" : "OFF"} | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Grounds AI reasoning with academic English precision, verified lexical definitions, grammar mastery, and the complete Oxford English syllabus textbooks for Grades 6, 7, and 8. | |
| </p> | |
| <div className="p-4 rounded-xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-amber-500/10 text-amber-400 flex items-center justify-center shrink-0"> | |
| <BookOpen className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white">Oxford Lexical & Syllabus Grounding</div> | |
| <div className="text-[11px] text-slate-400">100% Academic & Lexical Precision across all conversations.</div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 shrink-0"> | |
| {onOpenOxfordModal && ( | |
| <button | |
| type="button" | |
| onClick={onOpenOxfordModal} | |
| className="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-amber-300 text-xs font-bold transition border border-slate-700 cursor-pointer" | |
| > | |
| Browse Oxford Library | |
| </button> | |
| )} | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (onToggleOxfordContext) onToggleOxfordContext(!isOxfordContextActive); | |
| }} | |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${ | |
| isOxfordContextActive ? "bg-amber-500 text-slate-950 font-black shadow-sm" : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| {isOxfordContextActive ? "ACTIVE" : "OFF"} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Section 3: Automatic Theme (Light at 6 AM, dark at 6 PM) */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Sun className="h-4 w-4 text-indigo-400" /> | |
| <h4 className="text-sm font-bold text-white">Automatic Theme (Light at 6 AM, Dark at 6 PM)</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-indigo-500/10 text-indigo-300 border border-indigo-500/20 font-bold"> | |
| {autoThemeEnabled ? "SCHEDULE ACTIVE" : "MANUAL MODE"} | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Automatically switches the visual interface between sunlight daytime clarity and eye-safe twilight darkness according to real-world circadian time. | |
| </p> | |
| <div className="p-4 rounded-xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div className="flex items-center gap-3"> | |
| <Clock className="h-5 w-5 text-indigo-400 shrink-0" /> | |
| <div> | |
| <div className="text-xs font-bold text-white">Circadian Auto Theme Switcher</div> | |
| <div className="text-[11px] text-slate-400">Light Mode at 6:00 AM ☀️ | Dark Mode at 6:00 PM 🌙</div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| disabled={!!(themeManualOverrideUntil && themeManualOverrideUntil > Date.now())} | |
| onClick={() => { | |
| if (onToggleAutoTheme) onToggleAutoTheme(!autoThemeEnabled); | |
| }} | |
| className={`px-4 py-2 rounded-xl text-xs font-bold transition shrink-0 cursor-pointer ${ | |
| themeManualOverrideUntil && themeManualOverrideUntil > Date.now() | |
| ? "bg-slate-800 text-slate-500 cursor-not-allowed" | |
| : autoThemeEnabled | |
| ? "bg-indigo-600 text-white shadow-md" | |
| : "bg-slate-800 text-slate-400 hover:text-white" | |
| }`} | |
| title={themeManualOverrideUntil && themeManualOverrideUntil > Date.now() ? "Automatic theme paused for 6 hours after manual toggle" : "Toggle automatic theme"} | |
| > | |
| {themeManualOverrideUntil && themeManualOverrideUntil > Date.now() ? "PAUSED (6h)" : autoThemeEnabled ? "ON" : "OFF"} | |
| </button> | |
| </div> | |
| <div className="p-4 rounded-xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div className="flex items-center gap-3"> | |
| <Sun className="h-5 w-5 text-amber-400 shrink-0" /> | |
| <div> | |
| <div className="text-xs font-bold text-white">Current Mode: {darkMode ? "Dark Mode 🌙" : "Light Mode ☀️"}</div> | |
| <div className="text-[11px] text-slate-400">Manual switch pauses circadian scheduling for 6 hours.</div> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (onToggleDarkMode) onToggleDarkMode(!darkMode); | |
| }} | |
| className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-amber-300 text-xs font-bold transition border border-slate-700 shrink-0 cursor-pointer" | |
| > | |
| Switch to {darkMode ? "Light" : "Dark"} Mode | |
| </button> | |
| </div> | |
| </div> | |
| {/* Section 4: Aura Chat Theme */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Palette className="h-4 w-4 text-purple-400" /> | |
| <h4 className="text-sm font-bold text-white">Aura Chat Theme</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-purple-500/10 text-purple-300 border border-purple-500/20 font-bold"> | |
| {AURA_THEMES.length} Themes Available | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Choose a visual theme for Aura AI Chat. Dynamically colors the header, ambient glow, buttons, bubbles, and interactive surfaces. | |
| </p> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| {AURA_THEMES.map((theme) => { | |
| const isSelected = (auraThemeId || "aura-nebula") === theme.id; | |
| return ( | |
| <button | |
| key={theme.id} | |
| type="button" | |
| onClick={() => onSelectAuraTheme && onSelectAuraTheme(theme.id)} | |
| className={`p-3.5 rounded-xl text-left border transition-all cursor-pointer flex flex-col justify-between group ${ | |
| isSelected | |
| ? "bg-slate-900 border-purple-500 ring-2 ring-purple-500/40 shadow-lg" | |
| : "bg-slate-900/60 border-slate-800 hover:border-slate-700 hover:bg-slate-900" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className={`font-bold text-xs bg-gradient-to-r ${theme.brandGradient} bg-clip-text text-transparent group-hover:${theme.brandHoverGradient}`}> | |
| {theme.name} | |
| </span> | |
| {isSelected && ( | |
| <span className="h-4 w-4 rounded-full bg-emerald-500 text-white flex items-center justify-center shrink-0"> | |
| <Check className="h-2.5 w-2.5 stroke-[3]" /> | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center justify-between gap-2 mt-2"> | |
| <div className="flex -space-x-1"> | |
| {theme.previewColors.map((col, idx) => ( | |
| <span | |
| key={idx} | |
| className="h-3.5 w-3.5 rounded-full border border-slate-900 shadow-xs" | |
| style={{ backgroundColor: col }} | |
| /> | |
| ))} | |
| </div> | |
| <span className="text-[9.5px] font-mono text-slate-400 group-hover:text-purple-300"> | |
| {isSelected ? "Active" : "Apply"} | |
| </span> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Section 5: AI Voice Engine */} | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Volume2 className="h-4 w-4 text-cyan-400" /> | |
| <h4 className="text-sm font-bold text-white">AI Voice Engine</h4> | |
| </div> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-cyan-500/10 text-cyan-300 border border-cyan-500/20 font-bold"> | |
| Auto-Detects Language & Best Voice | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 leading-relaxed"> | |
| Configure voice synthesis for Live Voice Mode and spoken answers. Automatically detects user language and pairs the best natural neural voice. | |
| </p> | |
| <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3"> | |
| <select | |
| value={selectedVoiceURI || ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| if (onSelectVoice) onSelectVoice(val); | |
| }} | |
| className="flex-1 px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-700 text-xs text-white font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer" | |
| > | |
| <option value="">✨ Auto-Detect Language & Best Voice (Default)</option> | |
| {voices.map((v) => ( | |
| <option key={v.voiceURI || v.name} value={v.voiceURI || v.name}> | |
| {v.name} ({v.lang}) {v.default ? " — System Default" : ""} | |
| </option> | |
| ))} | |
| </select> | |
| <button | |
| type="button" | |
| onClick={() => handleTestVoice(selectedVoiceURI)} | |
| className="px-4 py-2.5 rounded-xl bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 text-cyan-300 text-xs font-bold flex items-center justify-center gap-2 cursor-pointer transition active:scale-95 shrink-0" | |
| > | |
| <Volume2 className="h-4 w-4" /> | |
| <span>Test Voice 🔊</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Section 6: Change Account Password & Security */} | |
| {currentUser && ( | |
| <div className="bg-slate-950 p-5 rounded-2xl border border-slate-800/80 shadow-lg space-y-4"> | |
| <div className="flex items-center gap-2"> | |
| <Key className="h-4 w-4 text-amber-400" /> | |
| <h4 className="text-sm font-bold text-white">Change Account Password</h4> | |
| </div> | |
| <p className="text-xs text-slate-400"> | |
| Update security credentials for user @{currentUser.username}. Passwords must be at least 6 characters. | |
| </p> | |
| <form onSubmit={handleChangePasswordSubmit} className="space-y-3.5 max-w-md"> | |
| <div> | |
| <label className="text-[11px] font-bold text-slate-400 uppercase tracking-wider block mb-1"> | |
| Current Password | |
| </label> | |
| <input | |
| type="password" | |
| value={currentPasswordInput} | |
| onChange={(e) => setCurrentPasswordInput(e.target.value)} | |
| placeholder="Enter current password" | |
| className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500" | |
| required | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-[11px] font-bold text-slate-400 uppercase tracking-wider block mb-1"> | |
| New Password | |
| </label> | |
| <input | |
| type="password" | |
| value={newPasswordInput} | |
| onChange={(e) => setNewPasswordInput(e.target.value)} | |
| placeholder="Enter new password (min. 6 characters)" | |
| className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500" | |
| required | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-[11px] font-bold text-slate-400 uppercase tracking-wider block mb-1"> | |
| Confirm New Password | |
| </label> | |
| <input | |
| type="password" | |
| value={confirmPasswordInput} | |
| onChange={(e) => setConfirmPasswordInput(e.target.value)} | |
| placeholder="Confirm new password" | |
| className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500" | |
| required | |
| /> | |
| </div> | |
| {passwordStatusMsg && ( | |
| <div className={`p-3 rounded-xl text-xs flex items-center gap-2 font-medium ${ | |
| passwordStatusMsg.success ? "bg-emerald-500/10 text-emerald-300 border border-emerald-500/20" : "bg-red-500/10 text-red-300 border border-red-500/20" | |
| }`}> | |
| {passwordStatusMsg.success ? <CheckCircle2 className="h-4 w-4 shrink-0" /> : <AlertTriangle className="h-4 w-4 shrink-0" />} | |
| <span>{passwordStatusMsg.text}</span> | |
| </div> | |
| )} | |
| <button | |
| type="submit" | |
| disabled={isSubmittingPassword} | |
| className="px-5 py-2.5 rounded-xl bg-amber-500 hover:bg-amber-600 text-slate-950 font-bold text-xs flex items-center justify-center gap-2 cursor-pointer transition disabled:opacity-50 shadow-md" | |
| > | |
| {isSubmittingPassword ? <RefreshCw className="h-4 w-4 animate-spin" /> : <ShieldCheck className="h-4 w-4" />} | |
| <span>Update Account Password</span> | |
| </button> | |
| </form> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* PAYMENT & UPGRADE MODAL */} | |
| {upgradeModalTier && ( | |
| <div | |
| className="absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4" | |
| onClick={() => setUpgradeModalTier(null)} | |
| > | |
| <div | |
| className="bg-slate-900 border border-slate-700 rounded-3xl max-w-md w-full p-6 shadow-2xl space-y-4 text-white" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex items-center justify-between border-b border-slate-800 pb-3"> | |
| <div className="flex items-center gap-2"> | |
| <Award className="h-5 w-5 text-emerald-400" /> | |
| <h4 className="text-base font-bold">Upgrade to {TIER_CONFIGS[upgradeModalTier].name}</h4> | |
| </div> | |
| <button | |
| onClick={() => setUpgradeModalTier(null)} | |
| className="h-7 w-7 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center cursor-pointer" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| {paymentSuccessMsg ? ( | |
| <div className="py-6 text-center space-y-2"> | |
| <CheckCircle2 className="h-10 w-10 text-emerald-400 mx-auto" /> | |
| <p className="text-sm font-bold text-emerald-300">{paymentSuccessMsg}</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| <div className="flex gap-2 p-1 bg-slate-950 rounded-xl border border-slate-800"> | |
| <button | |
| onClick={() => setPaymentMethodTab("gpay")} | |
| className={`flex-1 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${ | |
| paymentMethodTab === "gpay" ? "bg-emerald-600 text-white" : "text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| Google Pay (9500076449) | |
| </button> | |
| <button | |
| onClick={() => setPaymentMethodTab("razorpay")} | |
| className={`flex-1 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer ${ | |
| paymentMethodTab === "razorpay" ? "bg-indigo-600 text-white" : "text-slate-400 hover:text-white" | |
| }`} | |
| > | |
| Razorpay Online | |
| </button> | |
| </div> | |
| {paymentMethodTab === "gpay" ? ( | |
| <div className="space-y-3 text-xs"> | |
| <div className="p-3 bg-slate-950 rounded-xl border border-slate-800 space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-slate-400">Google Pay Number:</span> | |
| <div className="flex items-center gap-1.5"> | |
| <strong className="font-mono text-emerald-400 text-sm">9500076449</strong> | |
| <button | |
| onClick={handleCopyGpayNumber} | |
| className="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 text-[10px] font-bold cursor-pointer" | |
| > | |
| {copiedGpay ? "Copied!" : "Copy"} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-slate-400">UPI ID:</span> | |
| <div className="flex items-center gap-1.5"> | |
| <strong className="font-mono text-emerald-400 text-sm">9500076449@okaxis</strong> | |
| <button | |
| onClick={handleCopyUpiId} | |
| className="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 text-[10px] font-bold cursor-pointer" | |
| > | |
| {copiedUpiId ? "Copied!" : "Copy"} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between pt-1 border-t border-slate-800 text-slate-300 font-bold"> | |
| <span>Amount to Pay:</span> | |
| <span className="text-white text-sm">₹{TIER_CONFIGS[upgradeModalTier].priceINR}</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-slate-400 font-semibold mb-1"> | |
| Enter 12-digit UPI UTR / Transaction ID: | |
| </label> | |
| <input | |
| type="text" | |
| placeholder="e.g. 423819283748" | |
| value={transactionIdInput} | |
| onChange={e => setTransactionIdInput(e.target.value)} | |
| className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-800 text-white font-mono text-xs focus:outline-none focus:border-emerald-500" | |
| /> | |
| <p className="text-[10px] text-slate-400 mt-1"> | |
| After paying on Google Pay to 9500076449, enter your UTR number above. Admin (Harsha) will verify and activate your tier! | |
| </p> | |
| </div> | |
| <div className="flex justify-end gap-2 pt-2"> | |
| <button | |
| type="button" | |
| onClick={() => setUpgradeModalTier(null)} | |
| className="px-3 py-2 rounded-xl bg-slate-800 text-slate-300 font-bold text-xs" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleSubmitUpiTransaction} | |
| disabled={isSubmittingTxn || !transactionIdInput.trim()} | |
| className="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-bold text-xs flex items-center gap-1.5 cursor-pointer shadow-md" | |
| > | |
| {isSubmittingTxn ? <RefreshCw className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} | |
| <span>Submit for Admin Approval</span> | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4 text-center py-4"> | |
| <CreditCard className="h-10 w-10 text-indigo-400 mx-auto" /> | |
| <div className="text-xs text-slate-300"> | |
| Instant automated payment checkout via Razorpay for <strong>₹{TIER_CONFIGS[upgradeModalTier].priceINR}</strong>. | |
| </div> | |
| <button | |
| onClick={() => handleRazorpayPayment(upgradeModalTier)} | |
| className="w-full py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs shadow-md cursor-pointer" | |
| > | |
| Pay ₹{TIER_CONFIGS[upgradeModalTier].priceINR} with Razorpay | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Custom Confirmation Modal for Hugging Face Pull / Push Safety */} | |
| {confirmHfModal && ( | |
| <div | |
| className="absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-fade-in" | |
| onClick={() => setConfirmHfModal(null)} | |
| > | |
| <div | |
| className="bg-slate-900 p-6 rounded-3xl max-w-md w-full flex flex-col border border-slate-700 shadow-2xl" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex items-center gap-3 text-amber-400 mb-3"> | |
| <div className="h-10 w-10 rounded-2xl bg-amber-500/20 border border-amber-500/30 flex items-center justify-center shrink-0"> | |
| <Database className="h-5 w-5" /> | |
| </div> | |
| <div> | |
| <h4 className="text-base font-bold text-white"> | |
| {confirmHfModal.action === "pull" ? "Pull from Hugging Face Bucket" : "Backup to Hugging Face Bucket"} | |
| </h4> | |
| <p className="text-xs text-slate-400 font-mono">{hfStatus?.repo || customHfRepo}</p> | |
| </div> | |
| </div> | |
| <div className="my-3 p-3.5 rounded-2xl bg-slate-950 border border-slate-800 text-xs space-y-2 text-slate-300"> | |
| {confirmHfModal.action === "pull" ? ( | |
| <> | |
| <p className="font-semibold text-amber-300 flex items-center gap-1.5"> | |
| <AlertTriangle className="h-4 w-4 shrink-0 text-amber-400" /> | |
| Overwrite Warning: | |
| </p> | |
| <p className="text-slate-300 leading-relaxed text-[11px]"> | |
| Pulling from Hugging Face will download the latest cloud snapshot and overwrite local storage files. | |
| </p> | |
| </> | |
| ) : ( | |
| <> | |
| <p className="font-semibold text-emerald-300 flex items-center gap-1.5"> | |
| <UploadCloud className="h-4 w-4 shrink-0 text-emerald-400" /> | |
| Cloud Backup Confirmation: | |
| </p> | |
| <p className="text-slate-300 leading-relaxed text-[11px]"> | |
| This will upload all current Aura Store account databases, chats, and media files to your Hugging Face Dataset repository: <strong className="text-amber-300 font-mono">{hfStatus?.repo || customHfRepo}</strong>. | |
| </p> | |
| </> | |
| )} | |
| </div> | |
| <div className="flex items-center justify-end gap-2.5 pt-3 border-t border-slate-800"> | |
| <button | |
| type="button" | |
| onClick={() => setConfirmHfModal(null)} | |
| className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| onClick={confirmHfModal.action === "pull" ? handleHfPull : handleHfPush} | |
| className={`px-4 py-2 rounded-xl text-white font-bold text-xs cursor-pointer shadow-md flex items-center gap-1.5 ${ | |
| confirmHfModal.action === "pull" ? "bg-amber-600 hover:bg-amber-500" : "bg-emerald-600 hover:bg-emerald-500" | |
| }`} | |
| > | |
| {confirmHfModal.action === "pull" ? <Download className="h-3.5 w-3.5" /> : <UploadCloud className="h-3.5 w-3.5" />} | |
| <span>{confirmHfModal.action === "pull" ? "Proceed with Pull" : "Proceed with Backup"}</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Delete Confirmation Modal */} | |
| {confirmDeleteModal && ( | |
| <div | |
| className="absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-fade-in" | |
| onClick={() => setConfirmDeleteModal(null)} | |
| > | |
| <div | |
| className="bg-slate-900 p-6 rounded-3xl max-w-md w-full flex flex-col border border-slate-700 shadow-2xl" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex items-center gap-3 text-red-400 mb-3"> | |
| <div className="h-10 w-10 rounded-2xl bg-red-500/20 border border-red-500/30 flex items-center justify-center shrink-0"> | |
| <Trash2 className="h-5 w-5" /> | |
| </div> | |
| <div> | |
| <h4 className="text-base font-bold text-white"> | |
| Confirm Deletion ({confirmDeleteModal.itemsToDelete.length} {confirmDeleteModal.itemsToDelete.length === 1 ? "file" : "files"}) | |
| </h4> | |
| <p className="text-xs text-slate-400">Reclaim persistent storage space.</p> | |
| </div> | |
| </div> | |
| <p className="text-xs text-slate-300 leading-relaxed my-3"> | |
| Are you sure you want to permanently delete {confirmDeleteModal.itemsToDelete.length === 1 ? ( | |
| <strong className="text-white">{confirmDeleteModal.itemsToDelete[0].name}</strong> | |
| ) : ( | |
| <span>these {confirmDeleteModal.itemsToDelete.length} selected items</span> | |
| )}? This will reclaim storage from local /dataai and the Hugging Face dataset bucket. | |
| </p> | |
| <div className="flex items-center justify-end gap-2.5 pt-3 border-t border-slate-800"> | |
| <button | |
| type="button" | |
| onClick={() => setConfirmDeleteModal(null)} | |
| className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| onClick={executeDelete} | |
| className="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-500 text-white font-bold text-xs cursor-pointer shadow-md flex items-center gap-1.5" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| <span>Permanently Delete</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Media Preview Modal */} | |
| {previewItem && ( | |
| <div | |
| className="absolute inset-0 z-50 bg-black/90 backdrop-blur-md flex items-center justify-center p-4" | |
| onClick={() => setPreviewItem(null)} | |
| > | |
| <div | |
| className="bg-slate-900 border border-slate-700 rounded-3xl max-w-2xl w-full p-4 shadow-2xl flex flex-col max-h-[85vh]" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex items-center justify-between pb-3 border-b border-slate-800"> | |
| <h4 className="text-xs font-bold text-white truncate max-w-md">{previewItem.name}</h4> | |
| <button | |
| onClick={() => setPreviewItem(null)} | |
| className="h-7 w-7 rounded-full bg-slate-800 text-slate-300 flex items-center justify-center cursor-pointer" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-auto py-4 flex items-center justify-center"> | |
| {previewItem.name.match(/\.(jpeg|jpg|gif|png|webp|svg)$/i) ? ( | |
| <img src={previewItem.dataUrl} alt={previewItem.name} className="max-h-[60vh] max-w-full rounded-xl object-contain shadow-lg" /> | |
| ) : ( | |
| <div className="p-4 bg-slate-950 rounded-xl font-mono text-xs text-slate-300 max-h-[50vh] overflow-auto whitespace-pre-wrap"> | |
| {previewItem.relativePath} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Hugging Face Bucket & Token Configuration Modal */} | |
| {showHfTokenModal && ( | |
| <div | |
| className="absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-fade-in" | |
| onClick={() => setShowHfTokenModal(false)} | |
| > | |
| <div | |
| className="bg-slate-900 border border-slate-700 rounded-3xl max-w-lg w-full p-6 shadow-2xl space-y-4 text-white" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex items-center justify-between pb-3 border-b border-slate-800"> | |
| <div className="flex items-center gap-2.5"> | |
| <div className="h-9 w-9 rounded-xl bg-amber-500/10 border border-amber-500/30 flex items-center justify-center text-amber-400"> | |
| <Database className="h-5 w-5" /> | |
| </div> | |
| <div> | |
| <h4 className="text-base font-bold">HF Bucket & /dataai Storage</h4> | |
| <p className="text-xs text-slate-400">Configure your Hugging Face Bucket persistence</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => setShowHfTokenModal(false)} | |
| className="h-8 w-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center cursor-pointer transition" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| <div className="space-y-3.5 text-xs"> | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-300 mb-1.5"> | |
| Hugging Face Dataset / Bucket ID | |
| </label> | |
| <input | |
| type="text" | |
| value={customHfRepo} | |
| onChange={e => setCustomHfRepo(e.target.value)} | |
| placeholder="e.g. harshasri28/harshasri28-Aura-Ai" | |
| className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white font-mono text-xs focus:outline-none focus:border-amber-500" | |
| /> | |
| <p className="text-[11px] text-slate-400 mt-1"> | |
| Files from <code className="text-amber-300 font-mono">/dataai</code> will automatically synchronize under the <code className="text-emerald-400 font-mono">dataai/</code> folder in this bucket. | |
| </p> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-300 mb-1.5"> | |
| Hugging Face Access Token (Write / Fine-Grained) | |
| </label> | |
| <input | |
| type="password" | |
| value={customHfToken} | |
| onChange={e => setCustomHfToken(e.target.value)} | |
| placeholder="hf_..." | |
| className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white font-mono text-xs focus:outline-none focus:border-amber-500" | |
| /> | |
| <p className="text-[11px] text-slate-400 mt-1"> | |
| Requires <span className="text-amber-300">write</span> permission to push files from /dataai to your dataset repository. | |
| </p> | |
| </div> | |
| {/* Connection Test Result */} | |
| {hfTestResult && ( | |
| <div className={`p-3.5 rounded-xl border text-xs space-y-1.5 ${ | |
| hfTestResult.success | |
| ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-200" | |
| : "bg-red-500/10 border-red-500/30 text-red-200" | |
| }`}> | |
| <div className="flex items-center gap-2 font-bold"> | |
| {hfTestResult.success ? ( | |
| <CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" /> | |
| ) : ( | |
| <AlertTriangle className="h-4 w-4 text-red-400 shrink-0" /> | |
| )} | |
| <span>{hfTestResult.message}</span> | |
| </div> | |
| {hfTestResult.storagePath && ( | |
| <div className="text-[11px] text-slate-300 space-y-0.5 pt-1 border-t border-slate-800/80 font-mono"> | |
| <div>Local Root: <strong className="text-amber-300">{hfTestResult.storagePath}</strong></div> | |
| <div>HF Bucket Path: <strong className="text-emerald-300">{hfTestResult.hfSubfolder}</strong></div> | |
| {hfTestResult.repoType && <div>Repo Type: <span className="capitalize">{hfTestResult.repoType}</span></div>} | |
| {hfTestResult.authenticatedUser && <div>HF User: <strong>{hfTestResult.authenticatedUser}</strong></div>} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Test Connection & Action Buttons */} | |
| <div className="pt-1 flex items-center justify-between gap-2"> | |
| <button | |
| type="button" | |
| onClick={handleTestHfConnection} | |
| disabled={hfTestLoading} | |
| className="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-amber-300 font-semibold text-xs flex items-center gap-1.5 cursor-pointer transition border border-slate-700 disabled:opacity-50" | |
| > | |
| <RefreshCw className={`h-3.5 w-3.5 ${hfTestLoading ? "animate-spin" : ""}`} /> | |
| <span>{hfTestLoading ? "Testing..." : "Test Connection & Access"}</span> | |
| </button> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => setShowHfTokenModal(false)} | |
| className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-xs cursor-pointer transition" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleSaveHfConfig} | |
| className="px-4 py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold text-xs cursor-pointer transition shadow-md" | |
| > | |
| Save Configuration | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |