import React, { useEffect, useRef, useState } from 'react'; import Icon from './Icon.jsx'; import { useStudio, questionsFor, readiness, contextKey, log, visibleWorkspaces } from './store.jsx'; import { makeDemoSuite, generationStages } from './generation.js'; import { Badge, Button, Empty, Modal, Note, Search } from './ui.jsx'; import Dashboard from './pages/Dashboard.jsx'; import Workspaces from './pages/Workspaces.jsx'; import Baselines from './pages/Baselines.jsx'; import Evidence from './pages/Evidence.jsx'; import Versions from './pages/Versions.jsx'; import Chat from './pages/Chat.jsx'; import Readiness from './pages/Readiness.jsx'; import Suites from './pages/Suites.jsx'; import Templates from './pages/Templates.jsx'; import Approvals from './pages/Approvals.jsx'; import Graph from './pages/Graph.jsx'; import Admin from './pages/Admin.jsx'; import Login from './pages/Login.jsx'; import './studio.css'; export const navigation = [ { group: 'TEAM SPACE', items: [ ['overview', 'Overview', 'overview'], ['workspaces', 'Workspaces', 'folder'], ['baselines', 'System Baselines', 'book'], ['templates', 'Template library', 'checkSquare'] ] }, { group: 'CURRENT WORKSPACE', items: [ ['workspace', 'CR evidence', 'file'], ['versions', 'Knowledge versions', 'branch'], ['chat', 'Workspace chat', 'chat'], ['readiness', 'Generation readiness', 'spark'], ['suites', 'Test suites', 'checkSquare'], ['graph', 'Dependency graph', 'graph'] ] }, { group: 'GOVERNANCE', items: [ ['approvals', 'System Baseline updates', 'shield'], ['admin', 'Administration', 'settings'] ] }, ]; const pages = navigation.flatMap(g => g.items); const readRoute = () => { const [page = 'overview', query = ''] = location.hash.slice(1).split('?'); return { page: page || 'overview', create: query === 'create' }; }; export default function App() { const { state: s, mutate, workspace: w, toast, notify, saveError } = useStudio(); const [route, setRoute] = useState(readRoute), [modal, setModal] = useState(''), [query, setQuery] = useState(''), [menuOpen, setMenuOpen] = useState(false), [teamOpen, setTeamOpen] = useState(false); const [dark, setDark] = useState(() => { try { return localStorage.getItem('ccil-preview-theme') === 'dark'; } catch { return false; } }); const main = useRef(null), { page, create } = route; const teamWorkspaces = visibleWorkspaces(s); const questions = teamWorkspaces.flatMap(w => questionsFor(s, w).filter(q => q.status !== 'resolved')); function navigate(value) { location.hash = value; setRoute(readRoute()); setMenuOpen(false); setTeamOpen(false); main.current?.scrollTo(0, 0); } useEffect(() => { const update = () => { setRoute(readRoute()); main.current?.scrollTo(0, 0); }; window.addEventListener('hashchange', update); return () => window.removeEventListener('hashchange', update); }, []); useEffect(() => { document.documentElement.dataset.theme = dark ? 'dark' : 'light'; try { localStorage.setItem('ccil-preview-theme', dark ? 'dark' : 'light'); } catch { /* Theme still works when storage is unavailable. */ } }, [dark]); useEffect(() => { const keys = e => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setQuery(''); setModal(m => m ? '' : 'command'); } if (e.key === 'Escape') { setTeamOpen(false); setMenuOpen(false); } }; window.addEventListener('keydown', keys); return () => window.removeEventListener('keydown', keys); }, []); useEffect(() => { const timers = s.jobs.filter(j => j.status === 'running').map(j => setTimeout(() => mutate(d => { const job = d.jobs.find(item => item.id === j.id); if (job?.status !== 'running') return; const owner = d.workspaces.find(w => w.id === job.workspaceId); if (!owner || contextKey(owner) !== job.context) { job.status = 'Cancelled'; job.stage = 'Knowledge context changed. Start a new run.'; return; } const scoped = { ...d, session: { ...d.session, team: owner.team } }; if (!job.provisional && !readiness(scoped, owner).ready) { job.status = 'Failed'; job.stage = 'Readiness changed. Review the current evidence before retrying.'; return; } job.stageIndex += 1; job.stage = generationStages[job.stageIndex]; if (job.stageIndex >= 3) { let suite; try { suite = makeDemoSuite(scoped, owner, job.templateId, job.provisional); } catch (error) { job.status = 'Failed'; job.stage = error.message; return; } if (!suite.tests.length) { job.status = 'Failed'; job.stage = 'No source-linked rules are available for the local generator.'; return; } d.suites.unshift(suite); job.status = 'Completed'; job.suiteId = suite.id; log(d, `Completed test suite for ${owner.ref}`); } }), 900)); return () => timers.forEach(clearTimeout); }, [s.jobs, s.workspaces, s.questions, s.documents, s.shares]); let content; switch (page) { case 'overview': content = <Dashboard navigate={navigate}/>; break; case 'workspaces': content = <Workspaces navigate={navigate} create={create}/>; break; case 'baselines': content = <Baselines navigate={navigate} create={create}/>; break; case 'workspace': content = <Evidence navigate={navigate}/>; break; case 'versions': content = <Versions navigate={navigate}/>; break; case 'chat': content = <Chat navigate={navigate}/>; break; case 'readiness': content = <Readiness navigate={navigate}/>; break; case 'suites': content = <Suites navigate={navigate}/>; break; case 'templates': content = <Templates navigate={navigate}/>; break; case 'approvals': content = <Approvals navigate={navigate}/>; break; case 'graph': content = <Graph navigate={navigate}/>; break; case 'admin': content = <Admin navigate={navigate}/>; break; case 'login': content = <Login navigate={navigate}/>; break; default: content = <Empty title="Page not found" description="Choose a page from the studio navigation." action={<Button onClick={() => navigate('overview')}>Back to overview</Button>}/>; } const baseline = s.baselines.find(b => b.id === w?.baselineId), title = pages.find(item => item[0] === page)?.[1] || 'Sign in'; function signOut() { mutate(d => { d.session.authenticated = false; }); navigate('login'); } if (page === 'login' || !s.session.authenticated) return <Login navigate={navigate}/>; return <div className="app-shell"><a className="skip-link" href="#main-content" onClick={e => { e.preventDefault(); document.getElementById('main-content')?.focus(); }}>Skip to content</a>{menuOpen && <button className="sidebar-backdrop" aria-label="Close navigation" onClick={() => setMenuOpen(false)}/>} <aside className={`sidebar ${menuOpen ? 'is-open' : ''}`}><button className="brand" onClick={() => navigate('overview')} aria-label="CCIL Test Studio home"><span className="brand-symbol"><Icon name="layers" size={24}/></span><span><strong>CCIL <span>Test Studio</span></strong><small>EVIDENCE. CLARITY. CONFIDENCE.</small></span></button><div className="team-switcher-wrap"><button className="team-switcher" onClick={signOut} aria-label="Change team or sign out"><span className="team-avatar">{s.session.team[0]}</span><span><strong>{s.session.team}</strong><small>Shared team workspace</small></span><Icon name="down" size={14}/></button></div><nav className="main-nav" aria-label="Main navigation">{navigation.map(group => <div className="nav-group" key={group.group}><span className="nav-group-label">{group.group}</span>{group.items.Source:https://dev.to/iamry02/ccil-test-studio-source-10-srcappshelljsx-kcd

Related posts