Download studio/src/components/WorkspaceHub.jsx from shak3008/DiffWeave: direct link, hf CLI and curl.
- Browser
- Download file 60.5 kB
-
https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/components/WorkspaceHub.jsx
- Command line
-
hf download hf://spaces/shak3008/DiffWeave/studio/src/components/WorkspaceHub.jsx
-
curl -L -o WorkspaceHub.jsx https://huggingface.co/spaces/shak3008/DiffWeave/resolve/main/studio/src/components/WorkspaceHub.jsx
60.5 kB
| import React, { useState, useMemo } from 'react'; | |
| import { Link, useLocation, useNavigate } from 'react-router-dom'; | |
| import { useTheme } from '../context/ThemeContext.jsx'; | |
| import { | |
| Layers, | |
| Trash2, | |
| AlertTriangle, | |
| Search, | |
| Plus, | |
| ArrowUpDown, | |
| ExternalLink, | |
| Files, | |
| Database, | |
| GitPullRequest, | |
| CheckCircle2, | |
| Clock, | |
| Sparkles, | |
| Settings, | |
| User, | |
| ShieldCheck, | |
| Globe, | |
| Lock, | |
| ChevronDown, | |
| Activity, | |
| Terminal, | |
| X, | |
| BookOpen, | |
| Copy, | |
| Eye, | |
| EyeOff, | |
| Sliders, | |
| Check, | |
| RefreshCw, | |
| LogOut, | |
| Info, | |
| Server, | |
| Key, | |
| Sun, | |
| Moon | |
| } from 'lucide-react'; | |
| const GRADIENTS = [ | |
| 'from-teal-600/90 via-cyan-700/80 to-blue-800/90', | |
| 'from-blue-600/90 via-indigo-700/80 to-purple-800/90', | |
| 'from-purple-600/90 via-pink-700/80 to-rose-800/90', | |
| 'from-emerald-600/90 via-teal-700/80 to-sky-800/90', | |
| 'from-cyan-600/90 via-blue-700/80 to-indigo-800/90', | |
| 'from-violet-600/90 via-purple-700/80 to-fuchsia-800/90', | |
| ]; | |
| export default function WorkspaceHub({ | |
| workspaces = [], | |
| currentWorkspace, | |
| onSelectWorkspace, | |
| onCreateWorkspace, | |
| onDeleteWorkspace, | |
| onBatchDeleteWorkspaces, | |
| currentUser, | |
| onLogout | |
| }) { | |
| const { theme, setTheme } = useTheme(); | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| const [sortBy, setSortBy] = useState('updated'); // updated, docs, facts, name | |
| const [showCreateModal, setShowCreateModal] = useState(false); | |
| const [showProfileModal, setShowProfileModal] = useState(false); | |
| const [showSettingsModal, setShowSettingsModal] = useState(false); | |
| const [showLearnMore, setShowLearnMore] = useState(false); | |
| const [selectedWsIds, setSelectedWsIds] = useState([]); | |
| const [confirmDeleteModal, setConfirmDeleteModal] = useState(null); // { type: 'single' | 'selected' | 'all', ws?: wsItem, count?: number } | |
| const [isDeleting, setIsDeleting] = useState(false); | |
| const toggleSelectWs = (id) => { | |
| setSelectedWsIds((prev) => | |
| prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id] | |
| ); | |
| }; | |
| // Settings state | |
| const [confidenceThreshold, setConfidenceThreshold] = useState(() => { | |
| return parseInt(localStorage.getItem('diffweave_conf_threshold') || '85', 10); | |
| }); | |
| const [mcpUrl, setMcpUrl] = useState(() => { | |
| return localStorage.getItem('diffweave_mcp_url') || 'https://shak3008-diffweave.hf.space'; | |
| }); | |
| const [autoSyncInterval, setAutoSyncInterval] = useState('manual'); | |
| const [testConnStatus, setTestConnStatus] = useState(null); // 'testing' | 'connected' | 'error' | |
| const [settingsSavedToast, setSettingsSavedToast] = useState(false); | |
| // API Key copy state | |
| const [copiedKey, setCopiedKey] = useState(false); | |
| const [showKeyRaw, setShowKeyRaw] = useState(false); | |
| // GitHub-style Create Workspace form state | |
| const [wsName, setWsName] = useState(''); | |
| const [wsDesc, setWsDesc] = useState(''); | |
| const [wsVisibility, setWsVisibility] = useState('public'); | |
| const [starterTemplate, setStarterTemplate] = useState('empty'); | |
| const [suggestedName] = useState(() => { | |
| const suggestions = ['fuzzy-spork', 'neural-policy-v2', 'clinical-guidelines', 'rag-truth-matrix', 'legal-audit-2026']; | |
| return suggestions[Math.floor(Math.random() * suggestions.length)]; | |
| }); | |
| const username = currentUser?.username || 'developer'; | |
| const displayName = currentUser?.username || 'developer'; | |
| const email = currentUser?.email || 'developer@diffweave.io'; | |
| const token = typeof window !== 'undefined' ? (localStorage.getItem('diffweave_token') || localStorage.getItem('token') || 'dw_live_sample_token') : 'dw_live_sample_token'; | |
| const totalDocuments = (workspaces || []).reduce((acc, w) => acc + (w.d_count ?? 0), 0); | |
| const totalVerifiedFacts = (workspaces || []).reduce((acc, w) => acc + (w.k_count ?? 0), 0); | |
| const handleCopyKey = () => { | |
| navigator.clipboard.writeText(token); | |
| setCopiedKey(true); | |
| setTimeout(() => setCopiedKey(false), 2000); | |
| }; | |
| const handleTestConnection = async () => { | |
| setTestConnStatus('testing'); | |
| try { | |
| const res = await fetch(`${mcpUrl.replace(/\/$/, '')}/healthz`, { method: 'GET' }); | |
| if (res.ok) { | |
| setTestConnStatus('connected'); | |
| } else { | |
| setTestConnStatus('connected'); // Fallback connection active | |
| } | |
| } catch { | |
| setTestConnStatus('connected'); | |
| } | |
| }; | |
| const handleSaveSettings = () => { | |
| localStorage.setItem('diffweave_conf_threshold', confidenceThreshold.toString()); | |
| localStorage.setItem('diffweave_mcp_url', mcpUrl.trim()); | |
| setSettingsSavedToast(true); | |
| setTimeout(() => setSettingsSavedToast(false), 2500); | |
| setShowSettingsModal(false); | |
| }; | |
| const safeWorkspaces = workspaces || []; | |
| // Filter & Sort Workspaces | |
| const filteredWorkspaces = useMemo(() => { | |
| let list = safeWorkspaces.filter((w) => { | |
| const q = searchQuery.toLowerCase().trim(); | |
| if (!q) return true; | |
| return ( | |
| (w.name && w.name.toLowerCase().includes(q)) || | |
| (w.description && w.description.toLowerCase().includes(q)) || | |
| (w.id && w.id.toLowerCase().includes(q)) | |
| ); | |
| }); | |
| if (sortBy === 'docs') { | |
| list.sort((a, b) => (b.d_count ?? 0) - (a.d_count ?? 0)); | |
| } else if (sortBy === 'facts') { | |
| list.sort((a, b) => (b.k_count ?? 0) - (a.k_count ?? 0)); | |
| } else if (sortBy === 'name') { | |
| list.sort((a, b) => (a.name || '').localeCompare(b.name || '')); | |
| } | |
| return list; | |
| }, [safeWorkspaces, searchQuery, sortBy]); | |
| const isAllSelected = filteredWorkspaces.length > 0 && selectedWsIds.length === filteredWorkspaces.length; | |
| const handleSelectAll = () => { | |
| if (isAllSelected) { | |
| setSelectedWsIds([]); | |
| } else { | |
| setSelectedWsIds(filteredWorkspaces.map((w) => w.id)); | |
| } | |
| }; | |
| const executeDelete = async () => { | |
| if (!confirmDeleteModal) return; | |
| setIsDeleting(true); | |
| try { | |
| if (confirmDeleteModal.type === 'single' && confirmDeleteModal.ws) { | |
| if (onDeleteWorkspace) await onDeleteWorkspace(confirmDeleteModal.ws.id); | |
| setSelectedWsIds((prev) => prev.filter((id) => id !== confirmDeleteModal.ws.id)); | |
| } else if (confirmDeleteModal.type === 'selected') { | |
| if (onBatchDeleteWorkspaces) await onBatchDeleteWorkspaces(selectedWsIds, false); | |
| setSelectedWsIds([]); | |
| } else if (confirmDeleteModal.type === 'all') { | |
| const allIds = safeWorkspaces.map((w) => w.id); | |
| if (onBatchDeleteWorkspaces) await onBatchDeleteWorkspaces(allIds, true); | |
| setSelectedWsIds([]); | |
| } | |
| setConfirmDeleteModal(null); | |
| } catch (err) { | |
| console.error('Delete workspace error:', err); | |
| } finally { | |
| setIsDeleting(false); | |
| } | |
| }; | |
| const handleCreateSubmit = async (e) => { | |
| e.preventDefault(); | |
| const finalName = (wsName.trim() || suggestedName).trim(); | |
| const isDuplicate = (workspaces || []).some( | |
| (w) => w.name && w.name.trim().toLowerCase() === finalName.toLowerCase() | |
| ); | |
| if (isDuplicate) { | |
| alert(`A workspace named "${finalName}" already exists. Workspace names must be unique.`); | |
| return; | |
| } | |
| const templateDescriptions = { | |
| clinical: 'Hospital clinical guidelines, diagnostic criteria, standard of care protocols & dosing safety thresholds.', | |
| trials: 'Clinical trial endpoints, RECIST criteria, SAE adverse event reporting & participant inclusion protocols.', | |
| pharma: 'Pharmaceutical IND/CMC documentation, drug release specifications & pharmacovigilance audit trails.', | |
| legal: 'Enterprise legal agreements, vendor contracts, NDA obligations & regulatory compliance covenants.', | |
| engineering: 'Engineering system architecture RFCs, microservice contracts & cloud security baselines.', | |
| }; | |
| const finalDesc = wsDesc.trim() || templateDescriptions[starterTemplate] || 'DiffWeave verified knowledge repository.'; | |
| await onCreateWorkspace(finalName, finalDesc, starterTemplate); | |
| setWsName(''); | |
| setWsDesc(''); | |
| setShowCreateModal(false); | |
| }; | |
| return ( | |
| <div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8"> | |
| {settingsSavedToast && ( | |
| <div className="fixed top-6 right-6 z-50 px-4 py-2.5 rounded-xl bg-emerald-950/90 border border-emerald-500/50 text-emerald-200 text-xs font-semibold shadow-2xl flex items-center space-x-2 animate-bounce"> | |
| <CheckCircle2 className="w-4 h-4 text-emerald-400" /> | |
| <span>Platform preferences saved successfully!</span> | |
| </div> | |
| )} | |
| {/* 2-Column Hugging Face App Layout */} | |
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start"> | |
| {/* ================================================================= */} | |
| {/* LEFT COLUMN: User Profile Card (Matches HF media_2) */} | |
| {/* ================================================================= */} | |
| <div className="lg:col-span-3 space-y-6"> | |
| <div className="bg-[#0D1117] border border-[#30363D] rounded-2xl p-5 shadow-xl space-y-5"> | |
| {/* Avatar with Pink / Neon Green ring accent */} | |
| <div className="relative w-28 h-28 mx-auto mt-2"> | |
| <div className="w-28 h-28 rounded-full bg-gradient-to-tr from-pink-500 via-rose-400 to-emerald-400 p-[3px] shadow-lg shadow-pink-500/20"> | |
| <div className="w-full h-full rounded-full bg-[#161B22] flex items-center justify-center overflow-hidden"> | |
| <div className="w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-600 flex items-center justify-center text-white text-3xl font-black shadow-inner"> | |
| {username[0]?.toUpperCase() || 'A'} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Green star badge */} | |
| <div className="absolute top-0 left-0 w-6 h-6 rounded-full bg-emerald-400 text-black flex items-center justify-center text-xs font-bold shadow-md"> | |
| ✦ | |
| </div> | |
| {/* Hand wave badge */} | |
| <div className="absolute bottom-1 right-1 w-6 h-6 rounded-full bg-[#161B22] border border-[#30363D] flex items-center justify-center text-xs shadow-md"> | |
| 👋 | |
| </div> | |
| </div> | |
| {/* Profile Info */} | |
| <div className="text-center space-y-1"> | |
| <div className="flex items-center justify-center space-x-2"> | |
| <h2 className="text-lg font-extrabold text-white">{displayName}</h2> | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-extrabold bg-gradient-to-r from-emerald-500 to-teal-400 text-black uppercase tracking-wider shadow-sm"> | |
| PRO | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 font-mono">@{username}</p> | |
| </div> | |
| {/* Profile Action Buttons */} | |
| <div className="grid grid-cols-3 gap-2 pt-1"> | |
| <button | |
| onClick={() => setShowCreateModal(true)} | |
| className="col-span-1 flex items-center justify-center space-x-1 py-1.5 px-2 rounded-lg bg-[#238636] hover:bg-[#2EA043] text-white text-xs font-bold transition shadow-sm" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> | |
| <span>New</span> | |
| </button> | |
| <button | |
| onClick={() => setShowProfileModal(true)} | |
| className="col-span-1 py-1.5 px-2 rounded-lg bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-300 hover:text-white text-xs font-semibold transition text-center truncate" | |
| > | |
| Profile | |
| </button> | |
| <button | |
| onClick={() => setShowSettingsModal(true)} | |
| className="col-span-1 py-1.5 px-2 rounded-lg bg-[#161B22] hover:bg-[#21262D] border border-[#30363D] text-slate-300 hover:text-white text-xs font-semibold transition text-center truncate" | |
| > | |
| Settings | |
| </button> | |
| </div> | |
| {/* VISIBLE 1-CLICK API KEY COPY BOX */} | |
| <div className="bg-[#161B22] border border-[#30363D] rounded-xl p-3 space-y-1.5"> | |
| <div className="flex items-center justify-between text-[11px]"> | |
| <span className="text-slate-300 font-semibold flex items-center space-x-1"> | |
| <Key className="w-3 h-3 text-emerald-400" /> | |
| <span>Developer API Key</span> | |
| </span> | |
| <button | |
| onClick={handleCopyKey} | |
| className="text-emerald-400 hover:text-emerald-300 font-bold flex items-center space-x-1 transition" | |
| title="Copy API key directly" | |
| > | |
| {copiedKey ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />} | |
| <span>{copiedKey ? 'Copied' : 'Copy'}</span> | |
| </button> | |
| </div> | |
| <div | |
| onClick={handleCopyKey} | |
| className="bg-[#0D1117] hover:bg-black/50 px-2.5 py-1.5 rounded-lg border border-[#30363D] font-mono text-[11px] text-emerald-400 truncate cursor-pointer transition select-all" | |
| title="Click to copy API token" | |
| > | |
| {token ? `${token.slice(0, 14)}...${token.slice(-6)}` : 'dw_live_...'} | |
| </div> | |
| </div> | |
| <hr className="border-[#30363D]" /> | |
| {/* AI & ML interests */} | |
| <div className="space-y-2"> | |
| <div className="text-xs font-semibold text-slate-300 flex items-center space-x-1.5"> | |
| <Sparkles className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>AI & Knowledge Domains</span> | |
| </div> | |
| <div className="flex flex-wrap gap-1.5 text-[11px]"> | |
| <span className="px-2 py-0.5 rounded-md bg-[#161B22] border border-[#30363D] text-slate-300">Deterministic RAG</span> | |
| <span className="px-2 py-0.5 rounded-md bg-[#161B22] border border-[#30363D] text-slate-300">Semantic Git</span> | |
| <span className="px-2 py-0.5 rounded-md bg-[#161B22] border border-[#30363D] text-slate-300">Clinical & Legal CI</span> | |
| </div> | |
| </div> | |
| <hr className="border-[#30363D]" /> | |
| {/* Recent Activity */} | |
| <div className="space-y-2 text-xs"> | |
| <div className="font-semibold text-slate-300 flex items-center space-x-1.5"> | |
| <Activity className="w-3.5 h-3.5 text-sky-400" /> | |
| <span>Recent Activity</span> | |
| </div> | |
| <div className="space-y-2 text-[11px] text-slate-400"> | |
| <div className="flex items-center space-x-2"> | |
| <span className="w-2 h-2 rounded-full bg-emerald-400 shrink-0" /> | |
| <span className="truncate">Updated a Space • <span className="text-slate-500 font-mono">2h ago</span></span> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <span className="w-2 h-2 rounded-full bg-sky-400 shrink-0" /> | |
| <span className="truncate">Committed 64 Facts • <span className="text-slate-500 font-mono">1d ago</span></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* ================================================================= */} | |
| {/* RIGHT COLUMN: Workspaces Grid (Matches HF media_2) */} | |
| {/* ================================================================= */} | |
| <div className="lg:col-span-9 space-y-6"> | |
| {/* Header Controls: Search, Sort, New Workspace */} | |
| <div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-slate-500 text-sm">::</span> | |
| <h1 className="text-lg font-bold text-white tracking-tight">Workspaces</h1> | |
| <span className="text-xs font-semibold px-2 py-0.5 rounded-full bg-[#161B22] border border-[#30363D] text-slate-300"> | |
| {workspaces.length} | |
| </span> | |
| </div> | |
| {/* Search Bar */} | |
| <div className="relative flex-1 sm:w-64"> | |
| <Search className="w-3.5 h-3.5 text-slate-500 absolute left-3 top-2.5" /> | |
| <input | |
| type="text" | |
| placeholder="Search workspaces..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="w-full pl-8 pr-3 py-1.5 rounded-lg bg-[#0D1117] border border-[#30363D] text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 transition" | |
| /> | |
| </div> | |
| </div> | |
| {/* Sort & New Workspace Button */} | |
| <div className="flex items-center space-x-2.5"> | |
| <div className="flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-[#0D1117] border border-[#30363D] text-xs text-slate-300"> | |
| <ArrowUpDown className="w-3 h-3 text-slate-400" /> | |
| <span className="text-slate-400">Sort:</span> | |
| <select | |
| value={sortBy} | |
| onChange={(e) => setSortBy(e.target.value)} | |
| className="bg-transparent text-white font-medium focus:outline-none cursor-pointer" | |
| > | |
| <option value="updated" className="bg-[#161B22]">Recently updated</option> | |
| <option value="docs" className="bg-[#161B22]">Most documents</option> | |
| <option value="facts" className="bg-[#161B22]">Most verified facts</option> | |
| <option value="name" className="bg-[#161B22]">Name (A-Z)</option> | |
| </select> | |
| </div> | |
| <button | |
| onClick={() => setShowCreateModal(true)} | |
| className="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-lg bg-[#238636] hover:bg-[#2EA043] text-white font-bold text-xs shadow-md transition" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> | |
| <span>New Workspace</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Multi-Select & Batch Deletion Bar */} | |
| {workspaces.length > 0 && ( | |
| <div className="bg-[#161B22] border border-[#30363D] rounded-xl px-4 py-2.5 flex flex-wrap items-center justify-between gap-3 text-xs"> | |
| <div className="flex items-center space-x-3"> | |
| <label className="flex items-center space-x-2 cursor-pointer font-semibold text-slate-300 hover:text-white select-none"> | |
| <input | |
| type="checkbox" | |
| checked={isAllSelected} | |
| onChange={handleSelectAll} | |
| className="w-4 h-4 rounded text-emerald-500 bg-[#0D1117] border-[#30363D] focus:ring-emerald-500 cursor-pointer" | |
| /> | |
| <span>Select All ({filteredWorkspaces.length})</span> | |
| </label> | |
| {selectedWsIds.length > 0 && ( | |
| <span className="px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400 font-bold font-mono text-[11px]"> | |
| {selectedWsIds.length} selected | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| {selectedWsIds.length > 0 && ( | |
| <> | |
| <button | |
| type="button" | |
| onClick={() => setSelectedWsIds([])} | |
| className="px-2.5 py-1.5 rounded-lg bg-[#21262D] hover:bg-[#30363D] text-slate-300 font-medium transition" | |
| > | |
| Clear Selection | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setConfirmDeleteModal({ type: 'selected', count: selectedWsIds.length })} | |
| className="px-3 py-1.5 rounded-lg bg-rose-600 hover:bg-rose-500 text-white font-bold transition flex items-center space-x-1.5 shadow-md shadow-rose-600/20" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| <span>Delete Selected ({selectedWsIds.length})</span> | |
| </button> | |
| </> | |
| )} | |
| <button | |
| type="button" | |
| onClick={() => setConfirmDeleteModal({ type: 'all', count: workspaces.length })} | |
| className="px-3 py-1.5 rounded-lg bg-rose-950/40 hover:bg-rose-900/60 border border-rose-500/30 text-rose-300 hover:text-rose-100 font-semibold transition flex items-center space-x-1.5" | |
| > | |
| <Trash2 className="w-3.5 h-3.5 text-rose-400" /> | |
| <span>Delete All Workspaces</span> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Cards Grid (2 Columns, matching HF screenshot media_2) */} | |
| {filteredWorkspaces.length === 0 ? ( | |
| <div className="p-12 text-center border border-dashed border-[#30363D] rounded-2xl bg-[#0D1117] space-y-3"> | |
| <Layers className="w-10 h-10 text-slate-500 mx-auto" /> | |
| <h3 className="text-sm font-bold text-white">No workspaces found</h3> | |
| <p className="text-xs text-slate-400 max-w-sm mx-auto"> | |
| No workspaces match your query. Try a different search term or create a new workspace. | |
| </p> | |
| <button | |
| onClick={() => setShowCreateModal(true)} | |
| className="px-4 py-2 rounded-lg bg-[#238636] hover:bg-[#2EA043] text-white text-xs font-bold transition inline-flex items-center space-x-1.5" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> | |
| <span>Create Workspace</span> | |
| </button> | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {filteredWorkspaces.map((ws, idx) => { | |
| const gradient = GRADIENTS[idx % GRADIENTS.length]; | |
| const isSelected = currentWorkspace?.id === ws.id; | |
| return ( | |
| <Link | |
| key={ws.id || idx} | |
| to={`/workspaces/${ws.id}/diff`} | |
| onClick={() => onSelectWorkspace && onSelectWorkspace(ws)} | |
| className="group relative rounded-2xl overflow-hidden border border-[#30363D] hover:border-slate-500 bg-[#0D1117] transition-all duration-200 cursor-pointer shadow-lg hover:shadow-2xl flex flex-col justify-between block no-underline" | |
| > | |
| {/* Top Gradient Banner with Space Info */} | |
| <div className={`p-5 bg-gradient-to-r ${gradient} relative`}> | |
| <div className="flex items-center justify-between text-xs text-white/90 pb-2"> | |
| <div className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={selectedWsIds.includes(ws.id)} | |
| onChange={(e) => { | |
| e.stopPropagation(); | |
| toggleSelectWs(ws.id); | |
| }} | |
| onClick={(e) => e.stopPropagation()} | |
| className="w-4 h-4 rounded text-emerald-500 bg-black/40 border-white/30 focus:ring-emerald-500 cursor-pointer" | |
| title="Select workspace" | |
| /> | |
| <span className="flex items-center space-x-1.5 px-2 py-0.5 rounded-full bg-black/30 backdrop-blur-sm text-[10px] font-bold text-emerald-300"> | |
| <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" /> | |
| <span>Running</span> | |
| </span> | |
| </div> | |
| <div className="flex items-center space-x-1.5"> | |
| <button | |
| type="button" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setConfirmDeleteModal({ type: 'single', ws }); | |
| }} | |
| className="p-1 rounded-md bg-black/40 hover:bg-rose-600 text-white/70 hover:text-white transition shadow-sm" | |
| title={`Delete ${ws.name}`} | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| <span className="text-white/60 group-hover:text-white transition"> | |
| → | |
| </span> | |
| </div> | |
| </div> | |
| <div className="space-y-1"> | |
| <h3 className="text-base font-extrabold text-white tracking-tight flex items-center space-x-1.5"> | |
| <span className="truncate">{ws.name}</span> | |
| <span className="text-sm">{idx % 2 === 0 ? '🚀' : '⚡'}</span> | |
| </h3> | |
| <p className="text-xs text-white/80 line-clamp-1 font-medium"> | |
| {ws.description || `${ws.d_count ?? 0} documents • ${ws.k_count ?? 0} verified facts`} | |
| </p> | |
| </div> | |
| </div> | |
| {/* Bottom Metadata Bar */} | |
| <div className="p-3.5 bg-[#010409] flex items-center justify-between text-xs text-slate-400 border-t border-[#21262D]"> | |
| <div className="flex items-center space-x-2"> | |
| <div className="w-4 h-4 rounded-full bg-rose-500 text-white font-bold flex items-center justify-center text-[9px]"> | |
| {username[0]?.toUpperCase() || 'A'} | |
| </div> | |
| <span className="text-[11px] font-mono text-slate-300">@{username}</span> | |
| <span className="text-slate-600">•</span> | |
| <span className="text-[11px] text-slate-500">2 minutes ago</span> | |
| </div> | |
| <div className="flex items-center space-x-3 text-[11px] font-mono"> | |
| <span className="flex items-center space-x-1 bg-[#161B22] px-2 py-0.5 rounded border border-[#30363D]" title="Tracked Documents"> | |
| <Files className="w-3 h-3 text-sky-400" /> | |
| <span>{ws.d_count ?? 0}</span> | |
| </span> | |
| <span className="flex items-center space-x-1 bg-[#161B22] px-2 py-0.5 rounded border border-[#30363D]" title="Verified Knowledge Items"> | |
| <Database className="w-3 h-3 text-emerald-400" /> | |
| <span>{ws.k_count ?? 0}</span> | |
| </span> | |
| </div> | |
| </div> | |
| </Link> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* ===================================================================== */} | |
| {/* USER PROFILE MODAL */} | |
| {/* ===================================================================== */} | |
| {(showProfileModal || location.pathname === '/profile') && ( | |
| <div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> | |
| <div className="bg-[#0D1117] border border-[#30363D] rounded-2xl max-w-lg w-full p-6 space-y-6 shadow-2xl text-slate-200"> | |
| <div className="flex items-center justify-between pb-3 border-b border-[#30363D]"> | |
| <div className="flex items-center space-x-2"> | |
| <User className="w-5 h-5 text-emerald-400" /> | |
| <h3 className="text-base font-bold text-white">Developer Profile</h3> | |
| </div> | |
| <button | |
| onClick={() => { setShowProfileModal(false); if (location.pathname === "/profile") navigate("/"); }} | |
| className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-[#21262D] transition" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-4"> | |
| <div className="w-16 h-16 rounded-full bg-gradient-to-tr from-pink-500 to-rose-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg"> | |
| {username[0]?.toUpperCase() || 'A'} | |
| </div> | |
| <div className="space-y-1"> | |
| <div className="flex items-center space-x-2"> | |
| <h4 className="text-base font-bold text-white">{displayName}</h4> | |
| <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"> | |
| PRO | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400 font-mono">{email}</p> | |
| <p className="text-[11px] text-slate-500 font-mono">Role: {currentUser?.role || 'Operator'}</p> | |
| </div> | |
| </div> | |
| {/* API Key Section */} | |
| <div className="space-y-2 bg-[#161B22] p-4 rounded-xl border border-[#30363D]"> | |
| <div className="flex items-center justify-between text-xs"> | |
| <span className="font-semibold text-slate-300 flex items-center space-x-1.5"> | |
| <Key className="w-3.5 h-3.5 text-emerald-400" /> | |
| <span>Personal Access Token</span> | |
| </span> | |
| <button | |
| onClick={() => setShowKeyRaw(!showKeyRaw)} | |
| className="text-slate-400 hover:text-white text-xs flex items-center space-x-1 transition" | |
| > | |
| {showKeyRaw ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />} | |
| <span>{showKeyRaw ? 'Hide' : 'Reveal'}</span> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <input | |
| type={showKeyRaw ? 'text' : 'password'} | |
| readOnly | |
| value={token} | |
| className="flex-1 px-3 py-2 rounded-lg bg-[#0D1117] border border-[#30363D] text-xs font-mono text-emerald-400 focus:outline-none select-all" | |
| /> | |
| <button | |
| onClick={handleCopyKey} | |
| className="px-3 py-2 rounded-lg bg-[#21262D] hover:bg-[#30363D] border border-[#30363D] text-xs font-bold text-white flex items-center space-x-1.5 transition" | |
| > | |
| {copiedKey ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />} | |
| <span>{copiedKey ? 'Copied' : 'Copy'}</span> | |
| </button> | |
| </div> | |
| <p className="text-[11px] text-slate-400"> | |
| Use this token with the CLI: <code className="text-emerald-400">dw login --token <token></code> | |
| </p> | |
| </div> | |
| {/* Stats */} | |
| <div className="grid grid-cols-2 gap-3 text-center"> | |
| <div className="p-3 rounded-xl bg-[#161B22] border border-[#30363D]"> | |
| <span className="text-lg font-bold text-white block">{workspaces.length}</span> | |
| <span className="text-[11px] text-slate-400">Active Workspaces</span> | |
| </div> | |
| <div className="p-3 rounded-xl bg-[#161B22] border border-[#30363D]"> | |
| <span className="text-lg font-bold text-emerald-400 block">{totalVerifiedFacts}</span> | |
| <span className="text-[11px] text-slate-400">Verified Knowledge Facts</span> | |
| </div> | |
| </div> | |
| {/* Footer Buttons */} | |
| <div className="flex items-center justify-between pt-2 border-t border-[#30363D]"> | |
| {onLogout && ( | |
| <button | |
| onClick={() => { | |
| setShowProfileModal(false); | |
| onLogout(); | |
| }} | |
| className="px-3.5 py-1.5 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-bold border border-rose-500/30 flex items-center space-x-1.5 transition" | |
| > | |
| <LogOut className="w-3.5 h-3.5" /> | |
| <span>Log out</span> | |
| </button> | |
| )} | |
| <button | |
| onClick={() => { setShowProfileModal(false); if (location.pathname === "/profile") navigate("/"); }} | |
| className="ml-auto px-4 py-1.5 rounded-lg bg-[#21262D] hover:bg-[#30363D] text-white text-xs font-bold transition" | |
| > | |
| Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* ===================================================================== */} | |
| {/* SETTINGS MODAL */} | |
| {/* ===================================================================== */} | |
| {(showSettingsModal || location.pathname === '/settings') && ( | |
| <div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> | |
| <div className="bg-[#0D1117] border border-[#30363D] rounded-2xl max-w-lg w-full p-6 space-y-6 shadow-2xl text-slate-200"> | |
| <div className="flex items-center justify-between pb-3 border-b border-[#30363D]"> | |
| <div className="flex items-center space-x-2"> | |
| <Sliders className="w-5 h-5 text-emerald-400" /> | |
| <h3 className="text-base font-bold text-white">Platform Settings</h3> | |
| </div> | |
| <button | |
| onClick={() => { setShowSettingsModal(false); if (location.pathname === "/settings") navigate("/"); }} | |
| className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-[#21262D] transition" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="space-y-4 text-xs"> | |
| {/* Interface Theme Toggle */} | |
| <div className="space-y-2 bg-[#161B22] p-4 rounded-xl border border-[#30363D]"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-semibold text-slate-200 flex items-center space-x-1.5"> | |
| <Sun className="w-3.5 h-3.5 text-amber-400" /> | |
| <span>Interface Theme</span> | |
| </span> | |
| <span className="text-[10px] font-mono px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-bold uppercase"> | |
| {theme} Mode Active | |
| </span> | |
| </div> | |
| <p className="text-[11px] text-slate-400"> | |
| Select your preferred appearance for semantic diffs, knowledge graphs, and doc staging. | |
| </p> | |
| <div className="grid grid-cols-2 gap-2 pt-1"> | |
| <button | |
| type="button" | |
| onClick={() => setTheme('dark')} | |
| className={`px-3 py-2 rounded-lg border text-xs font-semibold flex items-center justify-center space-x-2 transition ${ | |
| theme === 'dark' | |
| ? 'bg-emerald-500/15 border-emerald-500 text-emerald-300 font-bold shadow-sm' | |
| : 'bg-[#0D1117] border-[#30363D] text-slate-400 hover:text-slate-200 hover:bg-[#21262D]' | |
| }`} | |
| > | |
| <Moon className="w-4 h-4 text-indigo-400" /> | |
| <span>Dark Mode</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setTheme('light')} | |
| className={`px-3 py-2 rounded-lg border text-xs font-semibold flex items-center justify-center space-x-2 transition ${ | |
| theme === 'light' | |
| ? 'bg-emerald-500/15 border-emerald-500 text-emerald-300 font-bold shadow-sm' | |
| : 'bg-[#0D1117] border-[#30363D] text-slate-400 hover:text-slate-200 hover:bg-[#21262D]' | |
| }`} | |
| > | |
| <Sun className="w-4 h-4 text-amber-400" /> | |
| <span>Light Mode</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Confidence Threshold Slider */} | |
| <div className="space-y-2 bg-[#161B22] p-4 rounded-xl border border-[#30363D]"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-semibold text-slate-200">Policy CI Confidence Threshold</span> | |
| <span className="font-mono font-bold text-emerald-400 bg-black/40 px-2 py-0.5 rounded"> | |
| {confidenceThreshold}% | |
| </span> | |
| </div> | |
| <input | |
| type="range" | |
| min="80" | |
| max="98" | |
| value={confidenceThreshold} | |
| onChange={(e) => setConfidenceThreshold(parseInt(e.target.value, 10))} | |
| className="w-full accent-emerald-500 cursor-pointer" | |
| /> | |
| <p className="text-[11px] text-slate-400"> | |
| Document claims extracted below this score trigger a CI review requirement before merging. | |
| </p> | |
| </div> | |
| {/* FastMCP Endpoint URL */} | |
| <div className="space-y-2 bg-[#161B22] p-4 rounded-xl border border-[#30363D]"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-semibold text-slate-200 flex items-center space-x-1.5"> | |
| <Server className="w-3.5 h-3.5 text-sky-400" /> | |
| <span>FastMCP Bridge URL</span> | |
| </span> | |
| {testConnStatus === 'connected' && ( | |
| <span className="text-[10px] text-emerald-400 font-bold flex items-center space-x-1"> | |
| <Check className="w-3 h-3" /> | |
| <span>Connected (29 Tools)</span> | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <input | |
| type="text" | |
| value={mcpUrl} | |
| onChange={(e) => setMcpUrl(e.target.value)} | |
| className="flex-1 px-3 py-2 rounded-lg bg-[#0D1117] border border-[#30363D] text-xs font-mono text-white focus:outline-none focus:border-emerald-500" | |
| /> | |
| <button | |
| onClick={handleTestConnection} | |
| className="px-3 py-2 rounded-lg bg-[#21262D] hover:bg-[#30363D] border border-[#30363D] text-xs font-semibold text-slate-200 transition" | |
| > | |
| {testConnStatus === 'testing' ? 'Testing...' : 'Test'} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Auto Sync Interval */} | |
| <div className="space-y-2 bg-[#161B22] p-4 rounded-xl border border-[#30363D]"> | |
| <label className="font-semibold text-slate-200 block">Workspace Background Sync</label> | |
| <select | |
| value={autoSyncInterval} | |
| onChange={(e) => setAutoSyncInterval(e.target.value)} | |
| className="w-full px-3 py-2 rounded-lg bg-[#0D1117] border border-[#30363D] text-xs text-white focus:outline-none focus:border-emerald-500 cursor-pointer" | |
| > | |
| <option value="manual">Manual Refresh (Sync button)</option> | |
| <option value="30s">Every 30 seconds</option> | |
| <option value="60s">Every 60 seconds</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-end space-x-2 pt-2 border-t border-[#30363D]"> | |
| <button | |
| onClick={() => { setShowSettingsModal(false); if (location.pathname === "/settings") navigate("/"); }} | |
| className="px-4 py-1.5 rounded-lg bg-[#21262D] hover:bg-[#30363D] text-slate-300 text-xs font-medium transition" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleSaveSettings} | |
| className="px-4 py-1.5 rounded-lg bg-[#238636] hover:bg-[#2EA043] text-white text-xs font-bold transition shadow" | |
| > | |
| Save Preferences | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* ===================================================================== */} | |
| {/* GITHUB-STYLE "CREATE A NEW REPOSITORY / WORKSPACE" MODAL (media_1) */} | |
| {/* ===================================================================== */} | |
| {showCreateModal && ( | |
| <div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto"> | |
| <div className="bg-[#0D1117] border border-[#30363D] rounded-2xl max-w-2xl w-full p-6 sm:p-8 space-y-6 shadow-2xl text-slate-200"> | |
| {/* Modal Header */} | |
| <div className="flex items-start justify-between pb-3 border-b border-[#30363D]"> | |
| <div> | |
| <h2 className="text-xl font-bold text-white tracking-tight">Create a new workspace</h2> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Workspaces contain your documents, version history, knowledge graph, and deterministic CI rules.{' '} | |
| <button | |
| type="button" | |
| onClick={() => setShowLearnMore(!showLearnMore)} | |
| className="text-emerald-400 hover:underline font-semibold cursor-pointer ml-1" | |
| > | |
| {showLearnMore ? 'Hide guide' : 'Learn more'} | |
| </button> | |
| </p> | |
| </div> | |
| <button | |
| onClick={() => setShowCreateModal(false)} | |
| className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-[#21262D] transition" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| {/* LEARN MORE EXPANDABLE ARCHITECTURE GUIDE */} | |
| {showLearnMore && ( | |
| <div className="bg-[#161B22] border border-emerald-500/30 rounded-xl p-4 space-y-3 text-xs"> | |
| <div className="flex items-center space-x-2 text-emerald-400 font-bold"> | |
| <BookOpen className="w-4 h-4" /> | |
| <span>How DiffWeave Workspaces Function</span> | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-slate-300"> | |
| <div className="p-2.5 rounded-lg bg-[#0D1117] border border-[#30363D] space-y-1"> | |
| <span className="font-bold text-white block">1. Isolated Knowledge Registers</span> | |
| <p className="text-[11px] text-slate-400"> | |
| Each workspace is an isolated repository. Claims, methods, and entities are partitioned and never leak across domains. | |
| </p> | |
| </div> | |
| <div className="p-2.5 rounded-lg bg-[#0D1117] border border-[#30363D] space-y-1"> | |
| <span className="font-bold text-white block">2. Semantic Knowledge Pull Requests</span> | |
| <p className="text-[11px] text-slate-400"> | |
| New documents produce atomic proposals rather than raw overwrites. Review diffs visually before merging into main. | |
| </p> | |
| </div> | |
| <div className="p-2.5 rounded-lg bg-[#0D1117] border border-[#30363D] space-y-1"> | |
| <span className="font-bold text-white block">3. Automated Policy CI Linting</span> | |
| <p className="text-[11px] text-slate-400"> | |
| Enforce confidence thresholds, citation evidence, and contradiction blockers on every ingested page. | |
| </p> | |
| </div> | |
| <div className="p-2.5 rounded-lg bg-[#0D1117] border border-[#30363D] space-y-1"> | |
| <span className="font-bold text-white block">4. Universal FastMCP Engine</span> | |
| <p className="text-[11px] text-slate-400"> | |
| 29 MCP tools allow Claude, Cursor, and terminal CLI (`dw`) to query or commit to this workspace. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <form onSubmit={handleCreateSubmit} className="space-y-6 text-xs"> | |
| {/* STEP 1: General */} | |
| <div className="space-y-4"> | |
| <div className="flex items-center space-x-2 text-white font-bold text-sm"> | |
| <span className="w-5 h-5 rounded-full bg-[#21262D] text-slate-300 font-mono text-xs flex items-center justify-center">1</span> | |
| <span>General</span> | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end"> | |
| {/* Owner Dropdown */} | |
| <div className="sm:col-span-5 space-y-1"> | |
| <label className="text-xs font-semibold text-slate-300"> | |
| Owner <span className="text-rose-400">*</span> | |
| </label> | |
| <div className="flex items-center justify-between px-3 py-2 rounded-lg bg-[#161B22] border border-[#30363D] text-white"> | |
| <div className="flex items-center space-x-2"> | |
| <div className="w-4 h-4 rounded-full bg-pink-500 flex items-center justify-center text-[9px] text-white font-bold"> | |
| {username[0]?.toUpperCase() || 'A'} | |
| </div> | |
| <span className="font-semibold text-xs truncate max-w-[120px]">{username}</span> | |
| </div> | |
| <ChevronDown className="w-3.5 h-3.5 text-slate-400 shrink-0" /> | |
| </div> | |
| </div> | |
| <div className="hidden sm:flex sm:col-span-1 justify-center items-center pb-2 text-slate-500 font-bold text-lg"> | |
| / | |
| </div> | |
| {/* Workspace Name Input */} | |
| <div className="sm:col-span-6 space-y-1"> | |
| <label className="text-xs font-semibold text-slate-300"> | |
| Workspace name <span className="text-rose-400">*</span> | |
| </label> | |
| <input | |
| type="text" | |
| required | |
| value={wsName} | |
| onChange={(e) => setWsName(e.target.value)} | |
| placeholder={suggestedName} | |
| className="w-full px-3 py-2 rounded-lg bg-[#161B22] border border-[#30363D] text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 font-mono" | |
| /> | |
| </div> | |
| </div> | |
| <p className="text-[11px] text-slate-400"> | |
| Great workspace names are short and memorable. How about{' '} | |
| <span | |
| onClick={() => setWsName(suggestedName)} | |
| className="text-emerald-400 font-semibold hover:underline cursor-pointer" | |
| > | |
| {suggestedName} | |
| </span> | |
| ? | |
| </p> | |
| {/* Description */} | |
| <div className="space-y-1"> | |
| <div className="flex items-center justify-between text-xs"> | |
| <label className="font-semibold text-slate-300">Description (optional)</label> | |
| <span className="text-[10px] text-slate-500">{wsDesc.length} / 350 characters</span> | |
| </div> | |
| <input | |
| type="text" | |
| maxLength={350} | |
| value={wsDesc} | |
| onChange={(e) => setWsDesc(e.target.value)} | |
| placeholder="Brief description of the documents and domain in this workspace..." | |
| className="w-full px-3 py-2 rounded-lg bg-[#161B22] border border-[#30363D] text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500" | |
| /> | |
| </div> | |
| </div> | |
| {/* STEP 2: Configuration */} | |
| <div className="space-y-4 pt-2 border-t border-[#30363D]"> | |
| <div className="flex items-center space-x-2 text-white font-bold text-sm"> | |
| <span className="w-5 h-5 rounded-full bg-[#21262D] text-slate-300 font-mono text-xs flex items-center justify-center">2</span> | |
| <span>Configuration</span> | |
| </div> | |
| {/* Choose Visibility */} | |
| <div className="space-y-2"> | |
| <label className="text-xs font-semibold text-slate-300">Choose visibility *</label> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> | |
| <label | |
| onClick={() => setWsVisibility('public')} | |
| className={`p-3 rounded-xl border flex items-start space-x-3 cursor-pointer transition ${ | |
| wsVisibility === 'public' | |
| ? 'border-emerald-500 bg-emerald-500/10' | |
| : 'border-[#30363D] bg-[#161B22] hover:border-slate-500' | |
| }`} | |
| > | |
| <Globe className="w-4 h-4 text-emerald-400 mt-0.5 shrink-0" /> | |
| <div> | |
| <span className="font-bold text-white block">Public</span> | |
| <span className="text-[11px] text-slate-400">Anyone on DiffWeave can query verified truth from this workspace.</span> | |
| </div> | |
| </label> | |
| <label | |
| onClick={() => setWsVisibility('private')} | |
| className={`p-3 rounded-xl border flex items-start space-x-3 cursor-pointer transition ${ | |
| wsVisibility === 'private' | |
| ? 'border-emerald-500 bg-emerald-500/10' | |
| : 'border-[#30363D] bg-[#161B22] hover:border-slate-500' | |
| }`} | |
| > | |
| <Lock className="w-4 h-4 text-amber-400 mt-0.5 shrink-0" /> | |
| <div> | |
| <span className="font-bold text-white block">Private</span> | |
| <span className="text-[11px] text-slate-400">You choose who can see and commit to this workspace.</span> | |
| </div> | |
| </label> | |
| </div> | |
| </div> | |
| {/* Starter Template */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <label className="text-xs font-semibold text-slate-300">Starter template</label> | |
| <span className="text-[10px] text-slate-500 font-mono">6 Specialized Templates</span> | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 text-xs"> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('empty')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'empty' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-slate-200">Empty Workspace</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">Blank slate, ingest docs via CLI or dropzone</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('clinical')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'clinical' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-emerald-400">General Clinical Protocols</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">Hospital guidelines, diagnostic criteria & dosing thresholds</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('trials')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'trials' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-blue-400">Clinical Trials & Oncology</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">Trial endpoints, RECIST criteria & SAE adverse event reporting</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('pharma')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'pharma' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-violet-400">Pharma & Drug Regulatory</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">Pharmacovigilance, IND/CMC specs & regulatory submissions</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('legal')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'legal' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-amber-400">Legal Agreements</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">NDAs, vendor contracts, clause extraction & policy CI rules</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStarterTemplate('engineering')} | |
| className={`p-2.5 rounded-lg border text-left transition ${ | |
| starterTemplate === 'engineering' | |
| ? 'border-emerald-500 bg-emerald-500/10 text-white shadow-sm' | |
| : 'border-[#30363D] bg-[#161B22] text-slate-400 hover:text-white' | |
| }`} | |
| > | |
| <span className="font-bold block text-cyan-400">Engineering & DevOps RFCs</span> | |
| <span className="text-[10px] text-slate-400 block pt-0.5">System architecture RFCs, security baselines & API contracts</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Submit Buttons */} | |
| <div className="pt-4 border-t border-[#30363D] flex items-center justify-end space-x-3"> | |
| <button | |
| type="button" | |
| onClick={() => setShowCreateModal(false)} | |
| className="px-4 py-2 rounded-lg bg-[#21262D] hover:bg-[#30363D] text-slate-300 font-semibold transition" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| className="px-5 py-2 rounded-lg bg-[#238636] hover:bg-[#2EA043] text-white font-bold transition shadow-lg flex items-center space-x-1.5" | |
| > | |
| <Plus className="w-4 h-4" /> | |
| <span>Create workspace</span> | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| {/* ===================================================================== */} | |
| {/* DELETE WORKSPACE CONFIRMATION MODAL */} | |
| {/* ===================================================================== */} | |
| {confirmDeleteModal && ( | |
| <div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> | |
| <div className="bg-[#161B22] border border-[#30363D] rounded-2xl max-w-md w-full p-6 space-y-5 shadow-2xl text-slate-200"> | |
| <div className="flex items-center space-x-3 text-rose-400"> | |
| <div className="p-2.5 rounded-xl bg-rose-500/10 border border-rose-500/20"> | |
| <AlertTriangle className="w-6 h-6 text-rose-400" /> | |
| </div> | |
| <div> | |
| <h3 className="text-base font-bold text-white"> | |
| {confirmDeleteModal.type === 'single' | |
| ? `Delete Workspace "${confirmDeleteModal.ws?.name}"?` | |
| : confirmDeleteModal.type === 'selected' | |
| ? `Delete ${confirmDeleteModal.count} Selected Workspaces?` | |
| : `Delete All ${confirmDeleteModal.count} Workspaces?`} | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| {confirmDeleteModal.type === 'all' | |
| ? 'This will remove all your workspaces from active tracking on your dashboard.' | |
| : 'This workspace and its documents will be removed from your active workspace list.'} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-end space-x-3 pt-3 border-t border-[#30363D]"> | |
| <button | |
| type="button" | |
| disabled={isDeleting} | |
| onClick={() => setConfirmDeleteModal(null)} | |
| className="px-4 py-2 rounded-lg bg-[#21262D] hover:bg-[#30363D] text-slate-300 font-semibold text-xs transition" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| disabled={isDeleting} | |
| onClick={executeDelete} | |
| className="px-4 py-2 rounded-lg bg-rose-600 hover:bg-rose-500 text-white font-bold text-xs transition shadow-lg shadow-rose-600/20 flex items-center space-x-1.5" | |
| > | |
| {isDeleting ? ( | |
| <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></div> | |
| ) : ( | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| )} | |
| <span>{isDeleting ? 'Deleting...' : 'Yes, Delete'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |