import { useNavigate, useLocation, Link } from 'react-router-dom'; import { useAuth } from './context/AuthContext.jsx'; import React, { useState, useEffect, useCallback } from 'react'; import { api } from './api/client'; import Navbar from './components/Navbar'; import WorkspaceHub from './components/WorkspaceHub'; import WorkspaceEmptyState from './components/WorkspaceEmptyState'; import SemanticDiffViewer from './components/SemanticDiffViewer'; import PRReviewDeck from './components/PRReviewDeck'; import IngestionStaging from './components/IngestionStaging'; import KnowledgeGraph from './components/KnowledgeGraph'; import KnowledgeBase from './components/KnowledgeBase'; import PolicyRulesLab from './components/PolicyRulesLab'; import AuditLog from './components/AuditLog'; import AccessManagementModal from './components/AccessManagementModal'; import CreateWorkspacePage from './components/CreateWorkspacePage'; import { RefreshCw, CheckCircle2, AlertCircle, Sparkles, GitBranch, ArrowLeft } from 'lucide-react'; export default function App() { const { isAuthenticated, logout: authLogout, user: authUser } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const [workspaces, setWorkspaces] = useState([]); const [currentWorkspace, setCurrentWorkspace] = useState(null); // Defaults to null -> shows WorkspaceHub! const [activeTab, setActiveTab] = useState('diff'); const [currentUser, setCurrentUser] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [dismissedEmptyStateFor, setDismissedEmptyStateFor] = useState(null); const [showAccessModal, setShowAccessModal] = useState(false); // State data const [stats, setStats] = useState(null); const [documents, setDocuments] = useState([]); const [proposals, setProposals] = useState([]); const [diff, setDiff] = useState(null); const [graphData, setGraphData] = useState(null); const [knowledgeItems, setKnowledgeItems] = useState([]); const [rules, setRules] = useState([]); const [activity, setActivity] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [toast, setToast] = useState(null); const showToast = (message, type = 'success') => { setToast({ message, type }); setTimeout(() => setToast(null), 3500); }; // Initial load: fetch workspaces & user identity useEffect(() => { async function init() { try { const wsList = await api.getWorkspaces(); setWorkspaces(Array.isArray(wsList) ? wsList : []); } catch (err) { showToast(`Connecting to DiffWeave FastMCP Bridge...`, 'info'); } const localUser = api.getCurrentUser(); if (localUser) setCurrentUser(localUser); } init(); }, []); // Parse deep URL parameters for native browser Back/Forward & direct opening const pathname = location.pathname; const isNewWorkspacePage = pathname === '/new' || pathname === '/workspaces/new'; const pathParts = pathname.split('/').filter(Boolean); const isWsPath = pathParts[0] === 'workspaces' && pathParts[1] !== 'new'; const urlWsId = isWsPath ? pathParts[1] : null; const urlTab = isWsPath ? (pathParts[2] || 'diff') : 'diff'; const urlProposalId = (isWsPath && pathParts[2] === 'prs') ? pathParts[3] : null; // Synchronize state whenever URL or workspaces change useEffect(() => { if (isNewWorkspacePage) { if (currentWorkspace !== null) { setCurrentWorkspace(null); } } else if (urlWsId) { const match = workspaces.find((w) => w.id === urlWsId); if (match) { if (!currentWorkspace || currentWorkspace.id !== match.id) { setCurrentWorkspace(match); } } else { // Fallback placeholder while workspaces load or if direct deep link if (!currentWorkspace || currentWorkspace.id !== urlWsId) { setCurrentWorkspace({ id: urlWsId, name: urlWsId, d_count: 0, k_count: 0 }); } } if (urlTab && activeTab !== urlTab) { setActiveTab(urlTab); } } else if (pathname === '/' || pathname === '/workspaces' || pathname === '/profile' || pathname === '/settings') { if (currentWorkspace !== null) { setCurrentWorkspace(null); } } }, [pathname, isNewWorkspacePage, urlWsId, urlTab, workspaces]); // Update currentWorkspace reference if workspaces list gets fresh counts useEffect(() => { if (currentWorkspace?.id && workspaces.length > 0) { const fresh = workspaces.find((w) => w.id === currentWorkspace.id); if (fresh && (fresh.name !== currentWorkspace.name || fresh.d_count !== currentWorkspace.d_count || fresh.k_count !== currentWorkspace.k_count)) { setCurrentWorkspace((prev) => prev ? { ...prev, ...fresh } : fresh); } } }, [workspaces]); // Fetch workspace details based on active tab const refreshData = useCallback(async () => { if (!currentWorkspace?.id) return; const wsId = currentWorkspace.id; setRefreshing(true); try { if (activeTab === 'diff') { const diffRes = await api.getSemanticDiff(wsId); setDiff(diffRes); } if (activeTab === 'prs') { const prsRes = await api.getProposals(wsId); setProposals(Array.isArray(prsRes) ? prsRes : []); } if (activeTab === 'staging') { const docsRes = await api.getDocuments(wsId); setDocuments(Array.isArray(docsRes) ? docsRes : []); } if (activeTab === 'graph') { const graphRes = await api.getKnowledgeGraph(wsId); setGraphData(graphRes); } if (activeTab === 'knowledge') { const kRes = await api.getKnowledgeItems(wsId); setKnowledgeItems(Array.isArray(kRes) ? kRes : []); } if (activeTab === 'rules') { const rulesRes = await api.getRules(wsId); setRules(Array.isArray(rulesRes) ? rulesRes : []); } if (activeTab === 'audit') { const actRes = await api.getActivityFeed(wsId); setActivity(Array.isArray(actRes) ? actRes : []); } // Background status fetch for navbar counters try { const statusRes = await api.getWorkspaceStatus(wsId); if (statusRes?.stats) { setStats(statusRes.stats); if (statusRes.stats.total_documents !== undefined || statusRes.stats.knowledge_items !== undefined) { setCurrentWorkspace((prev) => { if (!prev) return prev; const newD = statusRes.stats.total_documents ?? prev.d_count; const newK = statusRes.stats.knowledge_items ?? prev.k_count; if (prev.d_count === newD && prev.k_count === newK) return prev; return { ...prev, d_count: newD, k_count: newK }; }); } } } catch { // silent fallback } } catch (err) { console.error('Error refreshing workspace data:', err); } finally { setRefreshing(false); } }, [currentWorkspace?.id, activeTab]); useEffect(() => { refreshData(); }, [refreshData]); // Live auto-polling: Polls active workspace data every 5 seconds so CLI actions // (dw add, dw commit, dw push) reflect instantly without manual browser refresh useEffect(() => { if (!currentWorkspace?.id) return; const interval = setInterval(() => { refreshData(); }, 5000); return () => clearInterval(interval); }, [currentWorkspace?.id, refreshData]); // Live auto-polling on WorkspaceHub: Polls workspaces list every 8 seconds // so document/fact counters update in real time as CLI commands run useEffect(() => { if (currentWorkspace?.id) return; const interval = setInterval(async () => { try { const wsList = await api.getWorkspaces(); if (Array.isArray(wsList)) setWorkspaces(wsList); } catch {} }, 8000); return () => clearInterval(interval); }, [currentWorkspace?.id]); useEffect(() => { if (authUser) { setCurrentUser(authUser); } else { const u = api.getCurrentUser(); if (u) setCurrentUser(u); } }, [authUser]); // Actions const handleCreateWorkspace = async (name, desc, template = 'empty', visibility = 'private') => { const cleanName = (name || '').trim(); if (!cleanName) { showToast('Workspace name cannot be empty', 'error'); return; } const isDuplicate = (workspaces || []).some( (w) => w.name && w.name.trim().toLowerCase() === cleanName.toLowerCase() ); if (isDuplicate) { showToast(`A workspace named "${cleanName}" already exists. Please choose a unique name.`, 'error'); return; } try { const newWs = await api.createWorkspace(cleanName, desc, template, visibility); const wsItem = { ...newWs, name: newWs.name || cleanName, description: newWs.description || desc, template: newWs.template || template, visibility: newWs.visibility || visibility, d_count: 0, k_count: 0 }; setWorkspaces((prev) => [wsItem, ...prev]); setCurrentWorkspace(wsItem); showToast(`Workspace "${cleanName}" created successfully!`); // Direct immediately into the newly created workspace navigate(`/workspaces/${wsItem.id}/diff`); return wsItem; } catch (err) { showToast(`Create failed: ${err.message}`, 'error'); throw err; } }; const handleDeleteWorkspace = async (wsId) => { try { // Optimistically remove from state setWorkspaces((prev) => prev.filter((w) => w.id !== wsId)); if (currentWorkspace?.id === wsId) { setCurrentWorkspace(null); navigate('/'); } showToast('Workspace deleted successfully.', 'info'); await api.deleteWorkspace(wsId); refreshData(); } catch (err) { showToast(`Delete failed: ${err.message}`, 'error'); refreshData(); } }; const handleBatchDeleteWorkspaces = async (wsIds = [], deleteAll = false) => { try { if (deleteAll) { setWorkspaces([]); setCurrentWorkspace(null); navigate('/'); showToast('All workspaces deleted.', 'info'); } else { const idSet = new Set(wsIds); setWorkspaces((prev) => prev.filter((w) => !idSet.has(w.id))); if (currentWorkspace && idSet.has(currentWorkspace.id)) { setCurrentWorkspace(null); navigate('/'); } showToast(`${wsIds.length} workspace(s) deleted.`, 'info'); } await api.batchDeleteWorkspaces(wsIds, deleteAll); refreshData(); } catch (err) { showToast(`Batch delete failed: ${err.message}`, 'error'); refreshData(); } }; const handleSelectWorkspace = (ws) => { if (!ws) { setCurrentWorkspace(null); navigate('/'); return; } setCurrentWorkspace(ws); const targetTab = ((ws.d_count === 0 && ws.k_count === 0) || ws.is_new) ? 'quickstart' : 'diff'; navigate(`/workspaces/${ws.id}/${targetTab}`); }; const handleTabChange = (tabId) => { setActiveTab(tabId); if (currentWorkspace?.id) { navigate(`/workspaces/${currentWorkspace.id}/${tabId}`); } }; const handleReview = async (proposalId, decision, comments = '') => { // 1. Identify proposal details for rich feedback const targetProp = (proposals || []).find((p) => p.id === proposalId || p.proposal_id === proposalId); const factSnippet = targetProp?.summary || targetProp?.proposed_changes?.value || targetProp?.proposed_changes?.proposed?.value || (targetProp?.id ? `PR #${targetProp.id.slice(0, 8)}` : 'Knowledge proposal'); // 2. Optimistic Update on proposals array setProposals((prev) => prev.map((p) => { if (p.id === proposalId || p.proposal_id === proposalId) { return { ...p, status: decision, reviewed_at: new Date().toISOString() }; } return p; }) ); // 3. Optimistic Update on diff object (filter out approved/rejected deltas) setDiff((prev) => { if (!prev) return prev; const keep = (item) => item.proposal_id !== proposalId && item.id !== proposalId; return { ...prev, additions: (prev.additions || []).filter(keep), changes: (prev.changes || []).filter(keep), conflicts: (prev.conflicts || []).filter(keep), removals: (prev.removals || []).filter(keep), }; }); // 4. Optimistic Update on stats setStats((prev) => { if (!prev) return prev; const pending = Math.max(0, (prev.pending_proposals ?? 1) - 1); const kCount = decision === 'APPROVED' ? (prev.knowledge_items ?? 0) + 1 : (prev.knowledge_items ?? 0); return { ...prev, pending_proposals: pending, knowledge_items: kCount, }; }); // 5. Rich Toast feedback const actionLabel = decision === 'APPROVED' ? 'Approved & Merged' : decision === 'REJECTED' ? 'Rejected' : 'Archived'; const preview = factSnippet.length > 50 ? `${factSnippet.slice(0, 50)}...` : factSnippet; showToast(`${actionLabel}: "${preview}"`, decision === 'REJECTED' ? 'info' : 'success'); // 6. Network call (sub-10ms direct DB update via FastAPI) try { await api.reviewProposal(currentWorkspace?.id, proposalId, decision, comments); refreshData(); } catch (err) { showToast(`Review failed: ${err.message}`, 'error'); refreshData(); } }; const handleBatchReview = async (decision) => { const pendingList = (proposals || []).filter((p) => !p.status || p.status === 'PENDING'); const count = pendingList.length || (stats?.pending_proposals ?? 0); // Optimistic Update setProposals((prev) => prev.map((p) => (!p.status || p.status === 'PENDING' ? { ...p, status: decision } : p))); setDiff({ additions: [], changes: [], conflicts: [], removals: [] }); setStats((prev) => { if (!prev) return prev; return { ...prev, pending_proposals: 0, knowledge_items: decision === 'APPROVED' ? (prev.knowledge_items ?? 0) + count : (prev.knowledge_items ?? 0), }; }); showToast(`Batch ${decision.toLowerCase()} applied to ${count} pending items!`, 'success'); try { const res = await api.batchReview(currentWorkspace.id, decision); refreshData(); } catch (err) { showToast(`Batch review failed: ${err.message}`, 'error'); refreshData(); } }; const handleUpload = async (file) => { try { const res = await api.uploadDocument(currentWorkspace.id, file); showToast(`Document ${file.name} staged!`); // Update local workspace count setCurrentWorkspace((prev) => prev ? { ...prev, d_count: (prev.d_count || 0) + 1 } : prev); refreshData(); return res; } catch (err) { showToast(`Upload failed: ${err.message}`, 'error'); throw err; } }; const handleUploadBatch = async (files, batchName) => { try { const res = await api.uploadBatchDocuments(currentWorkspace.id, files, batchName); showToast(`Batch "${res.batch?.name || 'Batch'}" staged (${files.length} docs)!`); setCurrentWorkspace((prev) => prev ? { ...prev, d_count: (prev.d_count || 0) + files.length } : prev); refreshData(); return res; } catch (err) { showToast(`Batch upload failed: ${err.message}`, 'error'); throw err; } }; const handleCreateRule = async (name, operator, condition, isBlocking) => { try { await api.createRule(currentWorkspace.id, name, operator, condition, isBlocking); showToast(`Policy rule "${name}" created.`); refreshData(); } catch (err) { showToast(`Rule creation failed: ${err.message}`, 'error'); } }; const handleDeleteRule = async (ruleId) => { try { await api.deleteRule(ruleId); showToast('Rule deleted.'); refreshData(); } catch (err) { showToast(`Rule deletion failed: ${err.message}`, 'error'); } }; const handleValidate = async () => { try { const valRes = await api.validateProposals(currentWorkspace.id); showToast(valRes.has_violations ? 'Policy violations detected!' : 'All CI policy checks passed!'); return valRes; } catch (err) { showToast(`Validation check failed: ${err.message}`, 'error'); throw err; } }; const handleLogout = () => { api.logout(); authLogout(); navigate('/login', { replace: true }); }; // Determine if workspace is completely empty (0 docs and 0 facts, and user hasn't explicitly dismissed the quickstart) const isWorkspaceEmpty = currentWorkspace && (currentWorkspace.d_count === 0 && currentWorkspace.k_count === 0) && dismissedEmptyStateFor !== currentWorkspace.id; return (
{/* Three-tier GitHub/Vercel style Navbar */} refreshData()} refreshing={refreshing} proposals={proposals} diff={diff} onOpenAccessModal={() => setShowAccessModal(true)} /> {/* Main Content Area */}
{/* =================================================================== */} {/* VIEW 0: Dedicated Create Workspace Page (/new or /workspaces/new) */} {/* =================================================================== */} {isNewWorkspacePage && ( )} {/* =================================================================== */} {/* VIEW 1: Workspaces Hub (When no ws is selected and not creating) */} {/* =================================================================== */} {!isNewWorkspacePage && !currentWorkspace && ( )} {/* =================================================================== */} {/* VIEW 2: Inside a Workspace */} {/* =================================================================== */} {currentWorkspace && (
{/* Workspace Context Bar */}
setCurrentWorkspace(null)} className="hover:text-emerald-400 transition flex items-center space-x-1 font-semibold" > Workspaces / Knowledge Branch: main • Workspace: {currentWorkspace.id}
setActiveTab('staging')} className="px-2.5 py-1 rounded-md bg-[#238636] hover:bg-[#2EA043] text-white font-semibold text-xs transition block no-underline" > + Stage Document
{/* Show GitHub-style CLI quickstart only when quickstart tab is selected */} {activeTab === 'quickstart' ? ( { setDismissedEmptyStateFor(currentWorkspace.id); handleTabChange('diff'); }} /> ) : ( <> {/* TAB 1: Semantic Diff Viewer (Default!) */} {activeTab === 'diff' && ( )} {/* TAB 2: Pull Requests Deck */} {activeTab === 'prs' && ( )} {/* TAB 3: Knowledge Graph DAG Canvas */} {activeTab === 'graph' && ( )} {/* TAB 4: Documents & Ingestion Staging */} {activeTab === 'staging' && ( )} {/* TAB 5: Policy Rules CI Lab */} {activeTab === 'rules' && ( )} {/* TAB 6: Master Truth Register */} {activeTab === 'knowledge' && ( !p.status || p.status.toUpperCase() === 'PENDING').length} onApproveAll={() => handleBatchReview('APPROVED')} onRefresh={refreshData} /> )} {/* TAB 7: Audit Timeline Log */} {activeTab === 'audit' && ( )} )}
)}
{/* Floating Toast Notification */} {toast && (
{toast.type === 'error' ? ( ) : ( )} {toast.message}
)} {/* Workspace Access & Collaborators Modal */} {showAccessModal && currentWorkspace && ( setShowAccessModal(false)} onWorkspaceUpdated={(updatedWs) => { setCurrentWorkspace(updatedWs); setWorkspaces((prev) => prev.map((w) => w.id === updatedWs.id ? updatedWs : w)); }} /> )}
); }