Download studio/src/components/Navbar.jsx from shak3008/DiffWeave: direct link, hf CLI and curl.
- Browser
- Download file 22.6 kB
-
https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/components/Navbar.jsx
- Command line
-
hf download hf://spaces/shak3008/DiffWeave/studio/src/components/Navbar.jsx
-
curl -L -o Navbar.jsx https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/components/Navbar.jsx
22.6 kB
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { Link } from 'react-router-dom'; | |
| import { useTheme } from '../context/ThemeContext.jsx'; | |
| import { | |
| FileDiff, | |
| GitPullRequest, | |
| Network, | |
| ShieldCheck, | |
| Files, | |
| Database, | |
| History, | |
| Key, | |
| Search, | |
| User, | |
| LogOut, | |
| ExternalLink, | |
| ChevronDown, | |
| Layers, | |
| CheckCircle2, | |
| Copy, | |
| Lock, | |
| Cpu, | |
| Plus, | |
| RefreshCw, | |
| Sparkles, | |
| Terminal, | |
| Grid, | |
| ArrowLeft, | |
| Sun, | |
| Moon, | |
| Settings, | |
| Users, | |
| Globe | |
| } from 'lucide-react'; | |
| import NotificationsCenter from './NotificationsCenter'; | |
| export default function Navbar({ | |
| workspaces, | |
| currentWorkspace, | |
| onSelectWorkspace, | |
| onCreateWorkspace, | |
| activeTab, | |
| onSelectTab, | |
| stats, | |
| currentUser, | |
| onLogout, | |
| onRefresh, | |
| refreshing, | |
| proposals = [], | |
| diff = null, | |
| onOpenAccessModal, | |
| onOpenGlobalSearch, | |
| onOpenUserProfile, | |
| showToast = () => {} | |
| }) { | |
| const { theme, toggleTheme } = useTheme(); | |
| const [showWsDropdown, setShowWsDropdown] = useState(false); | |
| const [copiedApiKey, setCopiedApiKey] = useState(false); | |
| const handleCopyApiKey = () => { | |
| const tok = localStorage.getItem("diffweave_token") || localStorage.getItem("token") || ""; | |
| if (tok) { | |
| navigator.clipboard.writeText(tok); | |
| setCopiedApiKey(true); | |
| setTimeout(() => setCopiedApiKey(false), 2000); | |
| } else { | |
| alert("No active API token found. Please sign in."); | |
| } | |
| }; | |
| const [showCreateModal, setShowCreateModal] = useState(false); | |
| const [newWsName, setNewWsName] = useState(''); | |
| const [showUserDropdown, setShowUserDropdown] = useState(false); | |
| const wsDropdownRef = useRef(null); | |
| const userDropdownRef = useRef(null); | |
| // Close dropdowns when clicking anywhere outside | |
| useEffect(() => { | |
| function handleClickOutside(event) { | |
| if (wsDropdownRef.current && !wsDropdownRef.current.contains(event.target)) { | |
| setShowWsDropdown(false); | |
| } | |
| if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) { | |
| setShowUserDropdown(false); | |
| } | |
| } | |
| document.addEventListener('mousedown', handleClickOutside); | |
| document.addEventListener('touchstart', handleClickOutside); | |
| return () => { | |
| document.removeEventListener('mousedown', handleClickOutside); | |
| document.removeEventListener('touchstart', handleClickOutside); | |
| }; | |
| }, []); | |
| const handleCreate = async (e) => { | |
| e.preventDefault(); | |
| const cleanName = newWsName.trim(); | |
| if (!cleanName) return; | |
| const isDuplicate = (workspaces || []).some( | |
| (w) => w.name && w.name.trim().toLowerCase() === cleanName.toLowerCase() | |
| ); | |
| if (isDuplicate) { | |
| alert(`A workspace named "${cleanName}" already exists. Workspace names must be unique.`); | |
| return; | |
| } | |
| await onCreateWorkspace(cleanName); | |
| setNewWsName(''); | |
| setShowCreateModal(false); | |
| }; | |
| const totalDocs = stats?.total_documents ?? currentWorkspace?.d_count ?? 0; | |
| const verifiedFacts = stats?.knowledge_items ?? stats?.total_knowledge_items ?? currentWorkspace?.k_count ?? 0; | |
| // Reactively compute pending PR count from live proposals prop | |
| const pendingProposalsCount = (proposals && proposals.length > 0) | |
| ? proposals.filter((p) => !p.status || p.status.toUpperCase() === 'PENDING').length | |
| : (stats?.pending_proposals ?? 0); | |
| // Reactively compute pending diff deltas from live diff prop | |
| const pendingDiffDeltasCount = diff | |
| ? ((diff.additions?.length || 0) + (diff.changes?.length || 0) + (diff.conflicts?.length || 0) + (diff.removals?.length || 0)) | |
| : pendingProposalsCount; | |
| const tabs = [ | |
| { id: 'diff', label: 'Semantic Diff', icon: FileDiff, badge: pendingDiffDeltasCount > 0 ? `${pendingDiffDeltasCount}` : null }, | |
| { id: 'prs', label: 'PR Review Deck', icon: GitPullRequest, badge: pendingProposalsCount > 0 ? `${pendingProposalsCount} pending` : null }, | |
| { id: 'graph', label: 'Knowledge Graph', icon: Network }, | |
| { id: 'staging', label: 'Ingestion Staging', icon: Files, badge: totalDocs > 0 ? `${totalDocs} docs` : null }, | |
| { id: 'rules', label: 'Policy Rules CI', icon: ShieldCheck }, | |
| { id: 'knowledge', label: 'Truth Register', icon: Database, badge: verifiedFacts > 0 ? `${verifiedFacts} facts` : null }, | |
| { id: 'audit', label: 'Audit & Provenance', icon: History }, | |
| { id: 'quickstart', label: 'CLI Quickstart', icon: Terminal }, | |
| ]; | |
| return ( | |
| <header className="sticky top-0 z-40 bg-[#010409] border-b border-[#30363D] shadow-md select-none"> | |
| {/* --------------------------------------------------------------------- */} | |
| {/* TIER 1: Global Platform Bar (#010409) */} | |
| {/* --------------------------------------------------------------------- */} | |
| <div className="px-4 sm:px-6 py-2.5 flex items-center justify-between border-b border-[#21262D]"> | |
| {/* Left: Brand Logo & Workspace Switcher */} | |
| <div className="flex items-center space-x-4"> | |
| <Link | |
| to="/" | |
| onClick={() => onSelectWorkspace && onSelectWorkspace(null)} | |
| className="flex items-center space-x-2.5 cursor-pointer group" | |
| title="Return to Workspaces Hub" | |
| > | |
| <div className="w-8 h-8 rounded-lg bg-gradient-to-tr from-emerald-500 via-teal-500 to-sky-500 p-[1px] shadow-sm group-hover:scale-105 transition"> | |
| <div className="w-full h-full bg-[#010409] rounded-[6px] flex items-center justify-center"> | |
| <Layers className="w-4 h-4 text-emerald-400" /> | |
| </div> | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className="font-extrabold text-sm text-white tracking-tight group-hover:text-emerald-300 transition"> | |
| DiffWeave | |
| </span> | |
| <span className="text-[10px] text-slate-400 font-mono -mt-1"> | |
| Document Git Platform | |
| </span> | |
| </div> | |
| </Link> | |
| <span className="text-slate-600 hidden sm:inline">/</span> | |
| {/* Hub Link Button */} | |
| <Link | |
| to="/" | |
| onClick={() => onSelectWorkspace && onSelectWorkspace(null)} | |
| className={`hidden sm:flex items-center space-x-1.5 px-2.5 py-1 rounded-md border text-xs font-semibold transition ${ | |
| !currentWorkspace | |
| ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400' | |
| : 'bg-[#161B22] hover:bg-[#21262D] border-[#30363D] text-slate-300 hover:text-white' | |
| }`} | |
| > | |
| <Grid className="w-3.5 h-3.5" /> | |
| <span>Workspaces</span> | |
| </Link> | |
| {/* Workspace Dropdown */} | |
| <div ref={wsDropdownRef} className="relative"> | |
| <button | |
| onClick={() => { | |
| setShowWsDropdown((prev) => !prev); | |
| setShowUserDropdown(false); | |
| }} | |
| className="flex items-center space-x-1.5 px-2.5 py-1 rounded-md bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-200 transition" | |
| > | |
| <span className={`w-2 h-2 rounded-full ${currentWorkspace ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500'}`} /> | |
| <span className="truncate max-w-[150px] text-xs font-semibold"> | |
| {currentWorkspace ? currentWorkspace.name : 'Select Workspace'} | |
| </span> | |
| <ChevronDown className="w-3 h-3 text-slate-400" /> | |
| </button> | |
| {showWsDropdown && ( | |
| <div className="absolute left-0 mt-1.5 w-64 bg-[#161B22] border border-[#30363D] rounded-xl shadow-2xl p-2 z-50 space-y-1"> | |
| <div className="px-2 py-1 text-[11px] font-semibold text-slate-400 border-b border-[#30363D]"> | |
| Document Workspaces ({workspaces.length}) | |
| </div> | |
| <div className="max-h-56 overflow-y-auto space-y-0.5 py-1"> | |
| {workspaces.map((w) => ( | |
| <Link | |
| key={w.id} | |
| to={`/workspaces/${w.id}/diff`} | |
| onClick={() => { | |
| setShowWsDropdown(false); | |
| onSelectWorkspace && onSelectWorkspace(w); | |
| }} | |
| className={`w-full text-left px-2.5 py-1.5 rounded text-xs transition flex items-center justify-between ${ | |
| currentWorkspace?.id === w.id | |
| ? 'bg-emerald-500/20 text-emerald-300 font-bold' | |
| : 'text-slate-300 hover:bg-[#21262D] hover:text-white' | |
| }`} | |
| > | |
| <div className="truncate pr-2"> | |
| <p className="truncate">{w.name}</p> | |
| <span className="text-[10px] text-slate-500 font-mono"> | |
| {w.d_count ?? 0} docs • {w.k_count ?? 0} facts | |
| </span> | |
| </div> | |
| {currentWorkspace?.id === w.id && ( | |
| <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 shrink-0" /> | |
| )} | |
| </Link> | |
| ))} | |
| </div> | |
| <div className="pt-1.5 border-t border-[#30363D]"> | |
| <Link | |
| to="/new" | |
| onClick={() => setShowWsDropdown(false)} | |
| className="w-full text-left px-2 py-1.5 rounded text-xs text-emerald-400 hover:bg-[#21262D] font-semibold flex items-center space-x-1.5 no-underline" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> | |
| <span>New Document Workspace...</span> | |
| </Link> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Right: Search, FastMCP Bridge status, Theme Quick Toggle & User Profile */} | |
| <div className="flex items-center space-x-2.5"> | |
| {/* Global Search Trigger (Ctrl+K) */} | |
| <button | |
| type="button" | |
| onClick={onOpenGlobalSearch} | |
| className="flex items-center space-x-2 px-3 py-1.5 rounded-xl bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] hover:border-slate-600 text-xs text-slate-400 hover:text-slate-200 transition font-mono" | |
| title="Global Search across workspaces & users (Ctrl+K)" | |
| > | |
| <Search className="w-3.5 h-3.5 text-emerald-400 shrink-0" /> | |
| <span className="hidden sm:inline">Search...</span> | |
| <kbd className="hidden sm:inline px-1.5 py-0.2 rounded bg-black/40 border border-[#30363D] text-[10px] text-slate-500 font-mono"> | |
| Ctrl+K | |
| </kbd> | |
| </button> | |
| {/* FastMCP Connected Pill */} | |
| <div className="hidden md:flex items-center space-x-1.5 px-2.5 py-1 rounded-full bg-[#161B22] border border-[#30363D] text-[11px] text-slate-400 font-mono"> | |
| <span className="w-2 h-2 rounded-full bg-emerald-400" /> | |
| <Cpu className="w-3 h-3 text-emerald-400" /> | |
| <span>FastMCP: Active</span> | |
| </div> | |
| {/* Dynamic Real-Time Notifications Center (YouTube style) */} | |
| <NotificationsCenter | |
| onSelectWorkspace={onSelectWorkspace} | |
| onOpenProfile={onOpenUserProfile} | |
| showToast={showToast} | |
| /> | |
| {/* Quick Light/Dark Mode Toggle */} | |
| <button | |
| onClick={toggleTheme} | |
| className="p-1.5 rounded-lg bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-300 hover:text-white transition flex items-center justify-center" | |
| title={`Switch to ${theme === 'dark' ? 'Light' : 'Dark'} Mode`} | |
| aria-label="Toggle interface theme" | |
| > | |
| {theme === 'dark' ? ( | |
| <Sun className="w-4 h-4 text-amber-400" /> | |
| ) : ( | |
| <Moon className="w-4 h-4 text-indigo-400" /> | |
| )} | |
| </button> | |
| {/* User Menu */} | |
| {currentUser ? ( | |
| <div ref={userDropdownRef} className="relative"> | |
| <button | |
| onClick={() => { | |
| setShowUserDropdown((prev) => !prev); | |
| setShowWsDropdown(false); | |
| }} | |
| className="flex items-center space-x-1.5 px-2.5 py-1 rounded-full bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-200 transition" | |
| > | |
| <div className="w-4 h-4 rounded-full bg-emerald-500 text-black font-bold flex items-center justify-center text-[9px]"> | |
| {currentUser.username ? currentUser.username[0].toUpperCase() : 'U'} | |
| </div> | |
| <span className="font-semibold text-xs truncate max-w-[100px]"> | |
| {currentUser.username || 'Evaluator'} | |
| </span> | |
| <ChevronDown className="w-3 h-3 text-slate-400" /> | |
| </button> | |
| {showUserDropdown && ( | |
| <div className="absolute right-0 mt-1.5 w-56 bg-[#161B22] border border-[#30363D] rounded-xl shadow-2xl p-2 z-50 space-y-1 text-xs"> | |
| <div className="px-2 py-1.5 border-b border-[#30363D]"> | |
| <p className="font-bold text-white">{currentUser.username || 'User'}</p> | |
| <p className="text-[11px] text-slate-400 font-mono truncate">{currentUser.email || 'user@docweave.io'}</p> | |
| </div> | |
| <Link | |
| to="/" | |
| onClick={() => setShowUserDropdown(false)} | |
| className="w-full text-left px-2 py-1.5 rounded text-slate-300 hover:bg-[#21262D] flex items-center space-x-2" | |
| > | |
| <Grid className="w-3.5 h-3.5 text-sky-400" /> | |
| <span>Workspaces Hub</span> | |
| </Link> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setShowUserDropdown(false); | |
| if (onOpenUserProfile) onOpenUserProfile(currentUser?.id || currentUser?.username); | |
| }} | |
| className="w-full text-left px-2 py-1.5 rounded text-slate-300 hover:bg-[#21262D] flex items-center space-x-2" | |
| > | |
| <User className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>Public Profile & Activity</span> | |
| </button> | |
| <Link | |
| to="/settings" | |
| onClick={() => setShowUserDropdown(false)} | |
| className="w-full text-left px-2 py-1.5 rounded text-slate-300 hover:bg-[#21262D] flex items-center space-x-2" | |
| > | |
| <Settings className="w-3.5 h-3.5 text-sky-400" /> | |
| <span>Platform Settings</span> | |
| </Link> | |
| <button | |
| onClick={() => { | |
| setShowUserDropdown(false); | |
| handleCopyApiKey(); | |
| }} | |
| className="w-full text-left px-2 py-1.5 rounded text-slate-300 hover:bg-[#21262D] flex items-center justify-between" | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <Key className="w-3.5 h-3.5 text-amber-400" /> | |
| <span>{copiedApiKey ? 'Copied Token!' : 'Copy API Key'}</span> | |
| </div> | |
| {copiedApiKey && <span className="text-[10px] text-emerald-400 font-bold">✓</span>} | |
| </button> | |
| <div className="pt-1 border-t border-[#30363D]"> | |
| <button | |
| onClick={() => { | |
| setShowUserDropdown(false); | |
| onLogout(); | |
| }} | |
| className="w-full text-left px-2 py-1.5 rounded text-rose-400 hover:bg-rose-950/30 flex items-center space-x-2" | |
| > | |
| <LogOut className="w-3.5 h-3.5" /> | |
| <span>Sign Out</span> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <Link | |
| to="/login" | |
| className="px-3 py-1 text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-500 rounded-md transition" | |
| > | |
| Sign In | |
| </Link> | |
| )} | |
| </div> | |
| </div> | |
| {/* --------------------------------------------------------------------- */} | |
| {/* TIER 2: Workspace Document Context Header (#0D1117) - If Workspace Active */} | |
| {/* --------------------------------------------------------------------- */} | |
| {currentWorkspace && ( | |
| <> | |
| <div className="px-4 sm:px-6 py-3.5 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| {/* Workspace Title & Badge */} | |
| <div className="flex items-center space-x-2.5"> | |
| <Link | |
| to="/" | |
| onClick={() => onSelectWorkspace && onSelectWorkspace(null)} | |
| className="p-1 rounded-md hover:bg-[#161B22] text-slate-400 hover:text-white transition" | |
| title="Back to All Workspaces" | |
| > | |
| <ArrowLeft className="w-4 h-4" /> | |
| </Link> | |
| <Files className="w-5 h-5 text-emerald-400" /> | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-white font-bold text-base sm:text-lg"> | |
| {currentWorkspace?.name} | |
| </span> | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono"> | |
| Deterministic RAG | |
| </span> | |
| {/* Visibility Badge (Clickable to open Share/Access Modal) */} | |
| <button | |
| type="button" | |
| onClick={onOpenAccessModal} | |
| className={`px-2 py-0.5 rounded-full text-[10px] font-semibold flex items-center space-x-1 border transition ${ | |
| currentWorkspace?.visibility === 'public' | |
| ? 'bg-sky-500/10 text-sky-400 border-sky-500/30 hover:bg-sky-500/20' | |
| : 'bg-amber-500/10 text-amber-300 border-amber-500/30 hover:bg-amber-500/20' | |
| }`} | |
| title="Click to manage workspace access & visibility" | |
| > | |
| {currentWorkspace?.visibility === 'public' ? ( | |
| <Globe className="w-3 h-3 text-sky-400" /> | |
| ) : ( | |
| <Lock className="w-3 h-3 text-amber-400" /> | |
| )} | |
| <span className="capitalize">{currentWorkspace?.visibility || 'Private'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Real Document Platform KPI Badges & Access Controls */} | |
| <div className="flex items-center space-x-2 text-xs font-semibold"> | |
| {/* Share / Access Management Button */} | |
| <button | |
| type="button" | |
| onClick={onOpenAccessModal} | |
| className="flex items-center space-x-1.5 px-2.5 py-1 rounded-md bg-[#161B22] hover:bg-[#21262D] border border-slate-700 text-slate-200 hover:text-white transition shadow-sm" | |
| title="Manage workspace access, members & API tokens" | |
| > | |
| <Users className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>Share</span> | |
| </button> | |
| <div className="flex items-center space-x-1.5 px-2.5 py-1 rounded-md bg-[#161B22] border border-[#30363D] text-slate-300"> | |
| <Files className="w-3.5 h-3.5 text-sky-400" /> | |
| <span>Tracked Docs:</span> | |
| <span className="text-white font-bold font-mono">{totalDocs}</span> | |
| </div> | |
| <div className="flex items-center space-x-1.5 px-2.5 py-1 rounded-md bg-[#161B22] border border-[#30363D] text-slate-300"> | |
| <Database className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>Verified Truth:</span> | |
| <span className="text-white font-bold font-mono">{verifiedFacts}</span> | |
| </div> | |
| <button | |
| onClick={onRefresh} | |
| className="p-1.5 rounded-md bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-400 hover:text-white transition" | |
| title="Refresh Workspace Data" | |
| > | |
| <RefreshCw className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin text-emerald-400' : ''}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* ----------------------------------------------------------------- */} | |
| {/* TIER 3: Workspace Navigation Tabs (GitHub-style deep Link tabs) */} | |
| {/* ----------------------------------------------------------------- */} | |
| <nav className="flex px-4 sm:px-6 space-x-1 overflow-x-auto border-t border-[#21262D] text-xs font-medium no-scrollbar"> | |
| {tabs.map((tab) => { | |
| const Icon = tab.icon; | |
| const isActive = activeTab === tab.id; | |
| return ( | |
| <Link | |
| key={tab.id} | |
| to={`/workspaces/${currentWorkspace.id}/${tab.id}`} | |
| onClick={() => onSelectTab && onSelectTab(tab.id)} | |
| className={`flex items-center space-x-2 px-3.5 py-2.5 border-b-2 transition whitespace-nowrap ${ | |
| isActive | |
| ? 'border-emerald-500 text-white font-bold' | |
| : 'border-transparent text-slate-400 hover:text-slate-200 hover:border-slate-600' | |
| }`} | |
| > | |
| <Icon className={`w-4 h-4 ${isActive ? 'text-emerald-400' : 'text-slate-500'}`} /> | |
| <span>{tab.label}</span> | |
| {tab.badge && ( | |
| <span | |
| className={`px-1.5 py-0.2 rounded-full text-[10px] font-mono ${ | |
| isActive | |
| ? 'bg-emerald-500/20 text-emerald-300 font-bold' | |
| : 'bg-[#21262D] text-slate-400' | |
| }`} | |
| > | |
| {tab.badge} | |
| </span> | |
| )} | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| </> | |
| )} | |
| </header> | |
| ); | |
| } | |