Download studio/src/App.jsx from shak3008/DiffWeave: direct link, hf CLI and curl.
- Browser
- Download file 26.4 kB
-
https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/App.jsx
- Command line
-
hf download hf://spaces/shak3008/DiffWeave/studio/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/App.jsx
26.4 kB
| 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 ( | |
| <div className="min-h-screen bg-[#080B11] text-slate-100 flex flex-col font-sans transition-colors duration-200"> | |
| {/* Three-tier GitHub/Vercel style Navbar */} | |
| <Navbar | |
| workspaces={workspaces} | |
| currentWorkspace={currentWorkspace} | |
| onSelectWorkspace={handleSelectWorkspace} | |
| onCreateWorkspace={handleCreateWorkspace} | |
| activeTab={activeTab} | |
| onSelectTab={handleTabChange} | |
| stats={stats} | |
| currentUser={currentUser} | |
| onLogout={handleLogout} | |
| onRefresh={() => refreshData()} | |
| refreshing={refreshing} | |
| proposals={proposals} | |
| diff={diff} | |
| onOpenAccessModal={() => setShowAccessModal(true)} | |
| /> | |
| {/* Main Content Area */} | |
| <main className="flex-1 w-full"> | |
| {/* =================================================================== */} | |
| {/* VIEW 0: Dedicated Create Workspace Page (/new or /workspaces/new) */} | |
| {/* =================================================================== */} | |
| {isNewWorkspacePage && ( | |
| <CreateWorkspacePage | |
| onCreateWorkspace={handleCreateWorkspace} | |
| workspaces={workspaces} | |
| currentUser={currentUser} | |
| /> | |
| )} | |
| {/* =================================================================== */} | |
| {/* VIEW 1: Workspaces Hub (When no ws is selected and not creating) */} | |
| {/* =================================================================== */} | |
| {!isNewWorkspacePage && !currentWorkspace && ( | |
| <WorkspaceHub | |
| workspaces={workspaces} | |
| currentWorkspace={currentWorkspace} | |
| onSelectWorkspace={handleSelectWorkspace} | |
| onCreateWorkspace={handleCreateWorkspace} | |
| onDeleteWorkspace={handleDeleteWorkspace} | |
| onBatchDeleteWorkspaces={handleBatchDeleteWorkspaces} | |
| currentUser={currentUser} | |
| /> | |
| )} | |
| {/* =================================================================== */} | |
| {/* VIEW 2: Inside a Workspace */} | |
| {/* =================================================================== */} | |
| {currentWorkspace && ( | |
| <div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6"> | |
| {/* Workspace Context Bar */} | |
| <div className="flex items-center justify-between text-xs text-slate-400"> | |
| <div className="flex items-center space-x-2"> | |
| <Link | |
| to="/" | |
| onClick={() => setCurrentWorkspace(null)} | |
| className="hover:text-emerald-400 transition flex items-center space-x-1 font-semibold" | |
| > | |
| <ArrowLeft className="w-3.5 h-3.5" /> | |
| <span>Workspaces</span> | |
| </Link> | |
| <span className="text-slate-600">/</span> | |
| <span className="font-semibold text-white flex items-center space-x-1"> | |
| <GitBranch className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>Knowledge Branch:</span> | |
| </span> | |
| <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-mono border border-emerald-500/20 font-bold"> | |
| main | |
| </span> | |
| <span className="text-slate-600">•</span> | |
| <span className="text-slate-400 font-mono text-[11px] truncate max-w-[200px] sm:max-w-none"> | |
| Workspace: {currentWorkspace.id} | |
| </span> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Link | |
| to={`/workspaces/${currentWorkspace.id}/staging`} | |
| onClick={() => 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 | |
| </Link> | |
| <button | |
| onClick={() => refreshData()} | |
| className="flex items-center space-x-1 px-2.5 py-1 rounded-md bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-300 hover:text-white transition" | |
| > | |
| <RefreshCw className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin text-emerald-400' : ''}`} /> | |
| <span>Sync</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Show GitHub-style CLI quickstart only when quickstart tab is selected */} | |
| {activeTab === 'quickstart' ? ( | |
| <WorkspaceEmptyState | |
| workspace={currentWorkspace} | |
| currentUser={currentUser} | |
| onUploadDocument={handleUpload} | |
| onRefresh={refreshData} | |
| onDismissEmptyState={() => { | |
| setDismissedEmptyStateFor(currentWorkspace.id); | |
| handleTabChange('diff'); | |
| }} | |
| /> | |
| ) : ( | |
| <> | |
| {/* TAB 1: Semantic Diff Viewer (Default!) */} | |
| {activeTab === 'diff' && ( | |
| <SemanticDiffViewer | |
| diffData={diff} | |
| onReviewProposal={handleReview} | |
| onBatchReview={handleBatchReview} | |
| loading={loading} | |
| /> | |
| )} | |
| {/* TAB 2: Pull Requests Deck */} | |
| {activeTab === 'prs' && ( | |
| <PRReviewDeck | |
| proposals={proposals} | |
| onReviewProposal={handleReview} | |
| onBatchReview={handleBatchReview} | |
| loading={loading} | |
| workspaceId={currentWorkspace.id} | |
| selectedProposalId={urlProposalId} | |
| /> | |
| )} | |
| {/* TAB 3: Knowledge Graph DAG Canvas */} | |
| {activeTab === 'graph' && ( | |
| <KnowledgeGraph graphData={graphData} loading={loading} onSelectTab={handleTabChange} /> | |
| )} | |
| {/* TAB 4: Documents & Ingestion Staging */} | |
| {activeTab === 'staging' && ( | |
| <IngestionStaging | |
| documents={documents} | |
| onUploadDocument={handleUpload} | |
| onUploadBatch={handleUploadBatch} | |
| currentWorkspace={currentWorkspace} | |
| currentUser={authUser} | |
| loading={loading} | |
| onSelectTab={handleTabChange} | |
| onRefresh={refreshData} | |
| /> | |
| )} | |
| {/* TAB 5: Policy Rules CI Lab */} | |
| {activeTab === 'rules' && ( | |
| <PolicyRulesLab | |
| rules={rules} | |
| currentWorkspace={currentWorkspace} | |
| onCreateRule={handleCreateRule} | |
| onDeleteRule={handleDeleteRule} | |
| onValidate={handleValidate} | |
| loading={loading} | |
| /> | |
| )} | |
| {/* TAB 6: Master Truth Register */} | |
| {activeTab === 'knowledge' && ( | |
| <KnowledgeBase | |
| knowledgeItems={knowledgeItems} | |
| currentWorkspace={currentWorkspace} | |
| loading={loading} | |
| searchQuery={searchQuery} | |
| onSearchChange={setSearchQuery} | |
| setActiveTab={handleTabChange} | |
| pendingProposalsCount={(proposals || []).filter((p) => !p.status || p.status.toUpperCase() === 'PENDING').length} | |
| onApproveAll={() => handleBatchReview('APPROVED')} | |
| onRefresh={refreshData} | |
| /> | |
| )} | |
| {/* TAB 7: Audit Timeline Log */} | |
| {activeTab === 'audit' && ( | |
| <AuditLog activityFeed={activity} loading={loading} /> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| )} | |
| </main> | |
| {/* Floating Toast Notification */} | |
| {toast && ( | |
| <div className="fixed bottom-6 right-6 z-50 animate-bounce"> | |
| <div | |
| className={`px-4 py-2.5 rounded-xl border shadow-2xl flex items-center space-x-2 text-xs font-medium backdrop-blur-md ${ | |
| toast.type === 'error' | |
| ? 'bg-rose-950/90 border-rose-500/50 text-rose-200' | |
| : 'bg-emerald-950/90 border-emerald-500/50 text-emerald-200' | |
| }`} | |
| > | |
| {toast.type === 'error' ? ( | |
| <AlertCircle className="w-4 h-4 text-rose-400" /> | |
| ) : ( | |
| <CheckCircle2 className="w-4 h-4 text-emerald-400" /> | |
| )} | |
| <span>{toast.message}</span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Workspace Access & Collaborators Modal */} | |
| {showAccessModal && currentWorkspace && ( | |
| <AccessManagementModal | |
| workspace={currentWorkspace} | |
| isOpen={showAccessModal} | |
| onClose={() => setShowAccessModal(false)} | |
| onWorkspaceUpdated={(updatedWs) => { | |
| setCurrentWorkspace(updatedWs); | |
| setWorkspaces((prev) => prev.map((w) => w.id === updatedWs.id ? updatedWs : w)); | |
| }} | |
| /> | |
| )} | |
| </div> | |
| ); | |
| } | |