// Proxa × Cascade Ridge Foods — responsive Hub page POC (org display name = "Cascade Ridge Foods"; sample data models the fictional Cascade Ridge Foods). const { useState, useRef, useEffect } = React; // Workspaces (hub groups) inside the active organization ("Cascade Ridge Foods"). // Each workspace is a collapsible list of hubs; workspaces have no page of // their own. Hub content is poured per the Cascade Ridge Playbook. const WORKSPACES = [ { key: 'department', label: 'Department', abbr: 'DEPT', hubs: ['Finance', 'Contracts'] }, { key: 'projects', label: 'Projects', abbr: 'PROJ', hubs: ['Diligence'] }, { key: 'assets', label: 'Assets', abbr: 'BLDG', hubs: ['Tenney North Park'] }, ]; // Two-letter code shown in the collapsed rail. Multi-word names take the // initial of the first two words (Evergreen Basin -> EB); single words take // their first two letters (Finance -> FI). function hubAbbr(name) { const w = name.trim().split(/\s+/); return (w.length >= 2 ? w[0][0] + w[1][0] : name.slice(0, 2)).toUpperCase(); } // Display names decoupled from the internal hub id (which many branches key // off). Rename the shown label without touching the identifier 'Finance'. const HUB_LABELS = { Finance: 'Finance', Contracts: 'Customer' }; // hubAbbr() would make "Studies & Cohorts" into "S&" — spell the lab hubs out. const HUB_ABBRS = { Finance: 'FI', Contracts: 'CU', Typing: 'TY', 'Studies & Cohorts': 'SC' }; function hubLabel(name) { return HUB_LABELS[name] || name; } function hubAbbrOf(name) { return HUB_ABBRS[name] || hubAbbr(hubLabel(name)); } const DIVISIONS = ['CRF2']; // Tiles on the hub home page; each one behaves as a tab over the panel below. const HUB_TILES = [ // Artifacts tile = Dashboard + Slides, switched by pills inside the panel. { key: 'outputs', label: 'Artifacts', count: '12 sources', Icon: IconArtifact }, { key: 'collections', label: 'Projects', count: '—', Icon: IconStack }, { key: 'model', label: 'Model', count: '—', Icon: IconChip }, { key: 'control', label: 'Control', count: '14 updates', Icon: IconSliders }, { key: 'library', label: 'Library', count: '526 items', Icon: IconLibrary }, // Tools = Calendar + Lists, switched by pills inside the panel. { key: 'tools', label: 'Tools', count: '—', Icon: IconTools }]; // Per-hub tile count overrides. const HUB_TILE_COUNTS = { 'Tenney North Park': { dashboard: '6 sources', control: '4 updates', library: '23 items', calendar: '19 tasks · 4 milestones', settings: '4 members', }, Finance: { dashboard: '11 products', model: '3-statement', control: '3 updates', library: '52 items', calendar: 'Jul-26 close', }, }; // Per-hub tile label overrides — TN's Calendar tile is the Project DP. const HUB_TILE_LABELS = { 'Tenney North Park': { calendar: 'Project' }, }; function tileLabel(tile, hub) { const o = HUB_TILE_LABELS[hub]; return (o && o[tile.key]) || tile.label; } function tileCount(tile, hub) { const o = HUB_TILE_COUNTS[hub]; if (o && o[tile.key]) return o[tile.key]; return tile.count; } /* ============ Desktop nav rail ============ */ function Rail({ expanded, onToggleExpand, activeHub, onPickHub, onOpenGraph, graphOpen, onOpenAbout, aboutOpen, onGoHome, onOpenWorkspace, workspaceOpen, createdHubs = [], onCreateHub, groups = WORKSPACES, workspaceName, onOpenLanding, mark }) { // Workspace groups are all expanded by default. const [openGroups, setOpenGroups] = useState(() => Object.fromEntries(WORKSPACES.map((w) => [w.key, true]))); const toggleGroup = (k) => setOpenGroups((g) => ({ ...g, [k]: !g[k] })); const pick = (h) => { onPickHub(h); if (onGoHome) onGoHome(); }; /* ----- Collapsed icon rail (default) ----- */ if (!expanded) { return ( ); } /* ----- Expanded panel ----- */ return ( ); } /* ============ Brand wordmark ============ */ function BrandWordmark({ hub }) { if (hub === 'Tenney North Park') { return ( Tenney North Park ); } return ( Cascade Ridge Foods ); } /* ============ Hub home — ported tab pages (Control / Library / Calendar / Settings) rendered as isolated documents to avoid CSS/JS collisions with the host app (both use a `.su-*` source-updates vocabulary). ============ */ const HUB_TAB_KEYS = ['control', 'library', 'calendar', 'settings']; function HubTabContent({ tabKey, activeHub }) { const ref = useRef(null); useEffect(() => { const host = ref.current; if (!host) return; const html = (activeHub === 'Tenney North Park' && window.TN_TAB_HTML && window.TN_TAB_HTML[tabKey]) || (activeHub === 'Contracts' && window.CONTRACTS_TAB_HTML && window.CONTRACTS_TAB_HTML[tabKey]) || (window.HUB_TAB_HTML && window.HUB_TAB_HTML[tabKey]) || ''; host.innerHTML = html; const portal = document.getElementById('hubx-portal'); if (tabKey === 'library' && window.ProxaLibrary) { window.ProxaLibrary.init(host.querySelector('#sec-library')); } if (tabKey === 'settings') { // Reflect the active hub into the profile fields (image / name / // description) so Settings matches the hub the user is in. const meta = HUB_META[activeHub] || {}; const name = meta.title || activeHub; const desc = meta.desc || ''; const tile = host.querySelector('#logoTile'); if (tile) { tile.classList.remove('empty'); tile.innerHTML = '
' + hubAbbr(activeHub) + '
'; } const nm = host.querySelector('#hubName'); if (nm) nm.value = name; const ds = host.querySelector('#hubDesc'); if (ds) ds.value = desc; const ft = host.querySelector('#hubFootName'); if (ft) ft.textContent = name; // Move the fixed-position modal out to the portal so it escapes the // hub's container-type containing block and anchors to the viewport. if (portal) ['addScrim'].forEach((id) => { const m = host.querySelector('#' + id); if (m) portal.appendChild(m); }); if (window.renderUsers) window.renderUsers(); if (window.updateDescCount) window.updateDescCount(); } return () => { try { if (window.closeSourceUpdates) window.closeSourceUpdates(); } catch (e) {} try { if (window.closeRuns) window.closeRuns(); } catch (e) {} if (portal) ['addScrim'].forEach((id) => { const m = portal.querySelector('#' + id); if (m) m.remove(); }); document.body.style.overflow = ''; }; }, [tabKey, activeHub]); return
; } /* ============ Settings drawer ============ */ // Hub Settings lives in a right-side slide-over drawer (not a full tab page), // opened by the gear in the hub header. Native, drawer-width layout: a single // column of the fields that actually matter for a hub's identity. function SettingsForm({ activeHub, workspace, onClose }) { // `workspace` (a {name, empty} object) switches the form to workspace // settings — same layout, workspace identity instead of hub identity. const meta = workspace ? {} : (HUB_META[activeHub] || {}); const [name, setName] = useState(workspace ? workspace.name : (meta.title || activeHub)); const kindLabel = workspace ? 'Workspace' : 'Hub'; // Styling uses the CX drawer's own CSS vars so this reads as the same surface // as a data-product drawer. const inputStyle = { width: '100%', boxSizing: 'border-box', fontFamily: 'inherit', fontSize: 14, color: 'var(--foreground)', padding: '10px 12px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--background)', outline: 'none', }; const Label = ({ children, help }) => (
{children}
{help &&
{help}
}
); const detail = (k, v) => (
{k} {v}
); return (
{kindLabel} settings

{name || activeHub}

{kindLabel} profile
{hubAbbr(name || activeHub)}
Square logo, transparent background, ≥ 256 px.
setName(e.target.value)} />
Details
{detail('Owner', 'You')} {detail('Location', 'Proxa Server')} {detail('Members', workspace ? (workspace.empty ? '1' : '11') : '1')} {workspace && detail('Hubs', workspace.empty ? '0' : '4')}
); } // Reuses the data-product drawer's own chrome (.cx-root .drawer / .drawer-scrim // / .drawer-header / .drawer-body) so Settings IS the same drawer component as a // DP drawer — same width, slide animation and styling. Kept mounted so the // slide transition plays; the CX stylesheet is ensured in case no CX dashboard // has mounted yet (e.g. a brand-new empty hub). function SettingsDrawer({ open, activeHub, workspace, onClose }) { useEffect(() => { if (window.CX_DASH && !document.getElementById('cx-dash-style')) { const st = document.createElement('style'); st.id = 'cx-dash-style'; st.textContent = window.CX_DASH.css; document.head.appendChild(st); } }, []); useEffect(() => { if (!open) return; document.body.style.overflow = 'hidden'; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [open, onClose]); return (
{workspace ? 'Workspace settings' : 'Hub settings'}
); } /* ============ Hub home page ============ */ // Per-hub header (title + description). Only the Dashboard tab differs by hub; // Control / Library / Calendar / Settings are shared across all hubs. const HUB_META = { 'Tenney North Park': { title: 'Tenney North Park', desc: 'Pipeline, valuation and lease-up tracking for Tenney North Park, a 125-unit multi-housing asset in North Park, San Diego.', }, Finance: { title: 'Finance', desc: 'Corporate financial performance for Cascade Ridge Foods — revenue, margin, working capital, capacity and forecast, tied to the three-statement model and refreshed from the Jul-2026 close.', }, Contracts: { title: 'Customer', desc: 'The customer contract estate — MSAs, SOWs, renewals, obligations and pricing terms for Cascade Ridge Foods, sourced from the legal corpus and tied to revenue.', }, Diligence: { title: 'Diligence', desc: 'Deal pipeline, due-diligence workstreams and risk review for acquisitions and major pursuits.', }, }; const NH_ONBOARD_META = { title: 'Finance', desc: 'Your financial command center. Upload your model, GL and exports on the Dashboard — Claude reads them and builds your statements, working capital, budget and forecast as living data products.', }; function HubHome({ tileTab, setTileTab, onOpenPhoto, activeHub, onOpenAgentChat, onboarding, onAdvance, onPickTemplate, onOpenSettings, modelView, setModelView, hubTemplate, hubTitle, modeled, onUpload, domain }) { const activeTile = HUB_TILES.find((t) => t.key === tileTab) || HUB_TILES[0]; // Sections that hold sub-views, switched by pills inside the panel. // outputsView is local; modelView is lifted to App (so template-pick can land on Context). const [outputsView, setOutputsView] = React.useState('dashboard'); // 'dashboard' | 'slides' | 'collections' const [outputsViewMode, setOutputsViewMode] = React.useState('card'); // 'card' | 'list' (shared across Outputs) const [toolsView, setToolsView] = React.useState('calendar'); // 'calendar' | 'lists' const effTab = tileTab === 'outputs' ? outputsView : tileTab === 'tools' ? toolsView : tileTab; const firstInt = (s) => { const m = String(s || '').match(/\d+/); return m ? m[0] : null; }; const OUTPUTS_PILLS = [ { key: 'dashboard', label: 'Dashboard', Icon: IconLayoutDash, count: firstInt(tileCount({ key: 'dashboard', count: HUB_TILES.find((t) => t.key === 'outputs').count }, activeHub)) }, { key: 'slides', label: 'Slides', Icon: IconSlides, count: null }, ]; const MODEL_PILLS = [ { key: 'data', label: 'Data', Icon: IconBinary, count: null }, { key: 'context', label: 'Context', Icon: IconInfo, count: null }, { key: 'repo', label: 'Repo', Icon: IconCode, count: (activeHub === 'Finance' ? window.MODEL_REPO_COUNT : null) }, ]; const TOOLS_PILLS = [ { key: 'calendar', label: 'Calendar', Icon: IconCalendar, count: null }, { key: 'lists', label: 'Lists', Icon: IconChecklist, count: null }, ]; // Which pill row (if any) this section shows. const sectionPills = tileTab === 'outputs' ? { items: OUTPUTS_PILLS, val: outputsView, set: setOutputsView } : tileTab === 'model' ? { items: MODEL_PILLS, val: modelView, set: setModelView } : tileTab === 'tools' ? { items: TOOLS_PILLS, val: toolsView, set: setToolsView } : null; // Context = general (from the hub's template — the cookbook) + specific (this org's, if built). const modelTpl = activeHub === 'Finance' ? 'finance' : hubTemplate; const genCtx = (window.HUB_GENERAL_CONTEXT || {})[modelTpl]; // Derived specifics follow the domain: the lab pack carries its own, the // company pack falls back to the Cascade Ridge set. const domPack = window.proxaDomain ? window.proxaDomain(domain) : null; const specCtx = activeHub === 'Finance' ? window.FINANCE_SPECIFIC_CONTEXT : (modeled ? ((domPack && domPack.derivedSpecific) || window.FINANCE_DERIVED_SPECIFIC) : []); // A brand-new hub created via "+" (template not yet chosen) renders the standard // hub shell — banner + tiles — but empty, with a template picker in Artifacts. const isNewHub = onboarding && onboarding.step === 'template'; const meta = isNewHub ? { title: activeHub, desc: 'A new hub — empty for now. Pick a template and Claude builds its data products, or add files to the Library.' } : (onboarding ? NH_ONBOARD_META : (HUB_META[activeHub] || { title: hubTitle || activeHub, desc: 'A new hub.' })); const countFor = (t) => (onboarding ? '—' : tileCount(t, activeHub)); return (
{/* org identity lives in the rail; the header shows only the hub name */}
Hub

{meta.title}

{activeHub === 'Tenney North Park' ?
Tenney North Park — 2260 El Cajon Blvd at dusk
: null}
{HUB_TILES.map((t) => )}
{sectionPills && !isNewHub && !onboarding && (
{sectionPills.items.map((p) => )} {tileTab === 'outputs' && effTab !== 'slides' && (
{[['card', 'Card', <>], ['list', 'List', <>]].map(([k, label, path]) => )}
)}
)} {isNewHub ? ((effTab === 'dashboard' || tileTab === '') ?
This hub is empty

Pick a template below and Claude turns your files into data products — or upload to the Library to start from scratch.

:
Nothing here yet — this hub is empty.
) : onboarding ? : effTab === 'dashboard' ? {/* Lab hubs bring their own dashboards (lab_hub.jsx). */} {(window.LAB_HUB_DASH || {})[activeHub] ? React.createElement(window.LAB_HUB_DASH[activeHub]) : activeHub === 'Finance' /* CRF Finance DPA collection (Cascade Ridge Playbook §4); the H&V FinanceDashboard in finance_hub.jsx stays parked. */ ? (outputsViewMode === 'list' ? : ) : activeHub === 'Contracts' ? : activeHub === 'Tenney North Park' ? : activeHub === 'Diligence' ? : } : effTab === 'lists' ? : effTab === 'slides' ? (activeHub === 'Finance' ? : ) : tileTab === 'collections' ? : (tileTab === 'model' && modelView === 'context') ? (genCtx ? : ) : (tileTab === 'model' && modelView === 'repo') ? (activeHub === 'Finance' ? : ) : (activeHub === 'Contracts' && tileTab === 'model') ? : (activeHub === 'Contracts' && tileTab === 'control') ? : (activeHub === 'Diligence' && (tileTab === 'model' || tileTab === 'control' || tileTab === 'library')) ? : tileTab === 'model' ? (activeHub === 'Finance' || modeled ? : genCtx ? : ) : (effTab === 'calendar' && activeHub === 'Tenney North Park') ? : effTab === 'calendar' ? : }
); } /* ============ Mobile hub picker (bottom sheet) ============ */ function HubPickerSheet({ open, onClose, activeHub, onPickHub, onCreateHub, createdHubs = [], }) { const [orgOpen, setOrgOpen] = useState(false); const [openGroups, setOpenGroups] = useState(() => Object.fromEntries(WORKSPACES.map((w) => [w.key, true]))); const [dragY, setDragY] = useState(0); const dragStartY = useRef(null); useEffect(() => { if (!open) { setOrgOpen(false); setDragY(0); dragStartY.current = null; } }, [open]); useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [open, onClose]); const toggleGroup = (k) => setOpenGroups((g) => ({ ...g, [k]: !g[k] })); const pick = (h) => { if (onPickHub) onPickHub(h); onClose(); }; const create = () => { if (onCreateHub) onCreateHub(); onClose(); }; const onTouchStart = (e) => { dragStartY.current = e.touches[0].clientY; }; const onTouchMove = (e) => { if (dragStartY.current == null) return; const dy = e.touches[0].clientY - dragStartY.current; setDragY(Math.max(0, dy)); }; const onTouchEnd = () => { if (dragY > 100) onClose(); setDragY(0); dragStartY.current = null; }; if (!open) return null; const hubRow = (h) => (
); return ( <>
); } /* ============ Mobile bottom tab bar ============ */ function TabBar({ active, setActive, onOpenAgentChat, chatOpen, onOpenHubMenu, onCloseHubMenu, hubMenuOpen }) { return ( ); } /* ============ Floating chat bubble ============ */ function ChatBubble({ open, onToggle, side }) { return ( ); } /* ============ App ============ */ function App() { const [tab, setTab] = useState('hub'); // 'landing' | 'project' | 'collection' | 'record' | 'photo' | 'graph' | 'workspace' | 'about' const [view, setView] = useState('landing'); // the POC opens on the landing page const [railExpanded, setRailExpanded] = useState(false); // collapsed icon rail by default // Active hub persists across reloads (default: first hub in the rail) const [activeHub, setActiveHubRaw] = useState(() => { // validate the stored hub against the current rail (stale names from // earlier org skins fall back to the first hub) // Every built workspace's hubs, so a lab hub survives a reload too. const ALL_HUBS = WORKSPACES.flatMap((w) => w.hubs) .concat(((window.PROXA_DOMAINS || {}).lab?.built?.rail || []).flatMap((w) => w.hubs)); try { const stored = localStorage.getItem('proxa-active-hub'); return ALL_HUBS.includes(stored) ? stored : ALL_HUBS[0]; } catch (e) { return ALL_HUBS[0]; } }); window.__ACTIVE_HUB = activeHub; const setActiveHub = (h) => { setActiveHubRaw(h); window.__ACTIVE_HUB = typeof h === 'string' ? h : ''; try { localStorage.setItem('proxa-active-hub', typeof h === 'string' ? h : ''); } catch (e) {} }; // New Hub workflow (NEW-HUB.md): hubs created via `+` carry an onboarding flow // { step, template }. Since the `+` now lands on a real empty hub shell (the // empty state IS the product), EVERY created hub persists across reloads — // including one still at step 'template' (it reopens on its empty state). const [createdHubs, setCreatedHubs] = useState(() => { try { return Array.from(new Set(JSON.parse(localStorage.getItem('proxa.createdHubs') || '[]'))); } catch (e) { return []; } }); // Workspaces — Cascade Ridge is the built one; "New workspace" is an empty // first-time-user org (no hubs in the rail, empty tabs). // Two built workspaces: Cascade Ridge (a company) and the HLA Typing Lab (a // research lab). Both are real — same rail, tabs and hub shell; the lab is // there so the prototype isn't only a business. const [workspaces, setWorkspaces] = useState([ { id: 'cascade', name: 'Cascade Ridge Foods', builtIn: true }, { id: 'lab', name: 'HLA Typing Lab', builtIn: true }, ]); const [activeWorkspace, setActiveWorkspace] = useState('cascade'); const curWorkspace = workspaces.find((w) => w.id === activeWorkspace) || workspaces[0]; const wsIsEmpty = !!curWorkspace.empty; // Draft hubs for NEW workspaces live per-workspace (session-only); the built // Cascade Ridge workspace keeps using the persisted `createdHubs` list. const isCascade = activeWorkspace === 'cascade'; const [wsDraftHubs, setWsDraftHubs] = useState({}); const activeDrafts = isCascade ? createdHubs : (wsDraftHubs[activeWorkspace] || []); const addDraft = (name) => { if (isCascade) setCreatedHubs((hs) => [...hs, name]); else setWsDraftHubs((m) => ({ ...m, [activeWorkspace]: [...(m[activeWorkspace] || []), name] })); }; const renameDraft = (from, to) => { if (isCascade) setCreatedHubs((hs) => hs.map((h) => (h === from ? to : h))); else setWsDraftHubs((m) => ({ ...m, [activeWorkspace]: (m[activeWorkspace] || []).map((h) => (h === from ? to : h)) })); }; const removeDraft = (name) => { if (isCascade) setCreatedHubs((hs) => hs.filter((h) => h !== name)); else setWsDraftHubs((m) => ({ ...m, [activeWorkspace]: (m[activeWorkspace] || []).filter((h) => h !== name) })); }; // What each workspace is modelling (domains.jsx). Cascade Ridge is a company; // a new workspace has none until the user answers "What are you modelling?". const [wsDomains, setWsDomains] = useState({ cascade: 'company', lab: 'lab' }); const activeDomain = wsDomains[activeWorkspace]; const domPack = window.proxaDomain ? window.proxaDomain(activeDomain) : null; // Read by components that aren't passed the domain (the mobile hub sheet). window.__ACTIVE_DOMAIN = activeDomain; const pickDomain = (key) => setWsDomains((m) => ({ ...m, [activeWorkspace]: key })); const switchWorkspace = (id) => { setActiveWorkspace(id); setView('workspace'); }; // A new workspace is empty and domain-less — you answer "What are you // modelling?" inside it. Passing a domain (and name) skips that step, which // is how the landing page links straight to a seeded example. const createWorkspace = (domainKey, name) => { const id = 'ws-' + (workspaces.length + 1); const pack = domainKey && window.proxaDomain ? window.proxaDomain(domainKey) : null; setWorkspaces((list) => [...list, { id, name: name || (pack && pack.orgName) || 'New Workspace', empty: true }]); if (domainKey) setWsDomains((m) => ({ ...m, [id]: domainKey })); setActiveWorkspace(id); setView('workspace'); }; // Remove a created workspace (the built-in Cascade Ridge one can't be // removed) and fall back to Cascade Ridge if it was active. const removeWorkspace = (id) => { if ((workspaces.find((w) => w.id === id) || {}).builtIn) return; setWorkspaces((list) => list.filter((w) => w.id !== id)); setWsDraftHubs((m) => { const nm = { ...m }; delete nm[id]; return nm; }); setActiveWorkspace((cur) => (cur === id ? 'cascade' : cur)); }; const [hubFlow, setHubFlow] = useState(() => { try { return JSON.parse(localStorage.getItem('proxa.hubFlow') || '{}'); } catch (e) { return {}; } }); useEffect(() => { try { const hubs = Array.from(new Set(createdHubs)); const flow = {}; hubs.forEach((h) => { if (hubFlow[h]) flow[h] = hubFlow[h]; }); localStorage.setItem('proxa.createdHubs', JSON.stringify(hubs)); localStorage.setItem('proxa.hubFlow', JSON.stringify(flow)); } catch (e) {} }, [createdHubs, hubFlow]); const createHub = () => { const taken = new Set([...WORKSPACES.flatMap((w) => w.hubs), ...activeDrafts]); let name = 'New Hub', n = 2; while (taken.has(name)) { name = `New Hub ${n}`; n += 1; } addDraft(name); setHubFlow((f) => ({ ...f, [name]: { step: 'template', template: null } })); setView('project'); // New create-from-scratch entry: land on the "What should we build?" page // (rendered while flow.step === 'template'), not the Settings drawer. setTileTab(''); setActiveHub(name); setSettingsOpen(false); }; const pickTemplate = (templateId) => { if (templateId === '__create__') return; // "Create New Template" — stub for now // Diligence: rename the freshly-created hub to "Diligence" (it has real // identity + a dashboard) and enter its upload flow. if (templateId === 'diligence') { const from = activeHub; // Keep a single Diligence hub: if one already exists, drop the fresh // placeholder and switch to it instead of creating a duplicate. if (activeDrafts.includes('Diligence') && from !== 'Diligence') { removeDraft(from); setHubFlow((f) => { const nf = { ...f }; delete nf[from]; return nf; }); setActiveHub('Diligence'); return; } renameDraft(from, 'Diligence'); setHubFlow((f) => { const nf = { ...f }; delete nf[from]; nf['Diligence'] = { step: 'upload', template: 'diligence' }; return nf; }); setActiveHub('Diligence'); return; } // Model templates (finance, contracts, …): the hub is created, and you land // on Model → Context showing the template's GENERAL context (the cookbook), // with the assistant open telling you the next step — upload your model. applyTemplate(activeHub, templateId); }; // Shared tail of the template pick: flip `hub` to the built step, land on // Model → Context and open the assistant with the next-step message. const applyTemplate = (hub, templateId) => { const tplName = (templateId || 'hub').charAt(0).toUpperCase() + (templateId || 'hub').slice(1); setHubFlow((f) => ({ ...f, [hub]: { ...(f[hub] || {}), step: 'built', template: templateId, title: `${tplName} Hub`, modeled: false } })); setSettingsOpen(false); setTileTab('model'); setModelView('context'); setChatAgent('knowledge'); userPickedAgentRef.current = true; setAssistantWide(false); setOnboardMsg([ `Your ${tplName} hub is set up from the ${tplName} template — you're on its `, { bold: 'Context' }, ` page. The `, { bold: 'General' }, ` context is the cookbook: what a ${tplName} hub is and how it's normally built. It has no `, { bold: 'Specific' }, ` context yet — that's derived from your model.`, { br: true }, { br: true }, `Next step: upload ${templateId === 'finance' ? 'your financial model spreadsheet' : (window.proxaDomain(activeDomain).sourceNoun || 'your source files')} and I'll instantiate each general item into your specifics, for you to validate.`, ]); setChatOpen(true); }; // Empty-workspace template card → actually build the hub: create a draft in // the ACTIVE workspace named after the template, then run the template flow. const buildFromTemplate = (templateId, name) => { const base = name || (templateId || 'Hub').charAt(0).toUpperCase() + (templateId || 'Hub').slice(1); const taken = new Set([...WORKSPACES.flatMap((w) => w.hubs), ...activeDrafts]); let hub = base, n = 2; while (taken.has(hub)) { hub = `${base} ${n}`; n += 1; } addDraft(hub); setActiveHub(hub); setView('project'); applyTemplate(hub, templateId); }; const advanceStep = (step) => { setHubFlow((f) => ({ ...f, [activeHub]: { ...(f[activeHub] || {}), step } })); }; // Simulate the user uploading their model: Claude "reads" it, builds the data // products and derives Specific context (as Draft). Flips the hub to modeled. const simulateUpload = () => { const hub = activeHub; setHubFlow((f) => ({ ...f, [hub]: { ...(f[hub] || {}), modeled: true } })); setTileTab('model'); setModelView('context'); setChatAgent('knowledge'); userPickedAgentRef.current = true; setAssistantWide(false); const pack = window.proxaDomain(activeDomain); const firstFile = (pack.files && pack.files[0] && pack.files[0].name) || 'your files'; setOnboardMsg([ `Read your sources — `, { bold: firstFile }, `. I built the data products and derived `, { bold: '4 Specific context items' }, `, each instantiating a General cookbook item.`, { br: true }, { br: true }, `They're on the `, { bold: 'Context' }, ` page as `, { bold: 'Draft' }, ` — review each and `, { bold: 'Validate' }, ` to pin it as memory. Your model is now in `, { bold: 'Data' }, `.`, ]); setChatOpen(true); }; const [tileTab, setTileTab] = useState('outputs'); // hub-home active section tab const [modelView, setModelView] = useState('data'); // Model section sub-view: 'data' | 'context' | 'repo' const [onboardMsg, setOnboardMsg] = useState(null); // assistant instruction seeded on template-pick const [settingsOpen, setSettingsOpen] = useState(false); // Settings slide-over drawer const [settingsScope, setSettingsScope] = useState('hub'); // 'hub' | 'workspace' — what the drawer shows const [photoId, setPhotoId] = useState(null); const [photoFrom, setPhotoFrom] = useState('collection'); // 'collection' | 'record' const [collectionTab, setCollectionTab] = useState('overview'); // remembered for back nav const [chatOpen, setChatOpen] = useState(false); const [hubMenuOpen, setHubMenuOpen] = useState(false); // Prototype: how the assistant presents on desktop — 'dock' (side column) or // 'beneath' (page peels up to reveal the assistant surface underneath). const [assistantMode, setAssistantMode] = useState('beneath'); // Beneath surface is dual-width: narrow by default (quick chat, page barely // disturbed) and wide when a bigger canvas is needed (user expands it, or the // assistant expands it for output like a generated dashboard). const [assistantWide, setAssistantWide] = useState(false); const [chatWidth, setChatWidth] = useState(440); // 375..700 const [chatAgent, setChatAgent] = useState('knowledge'); const [pendingChatPrompt, setPendingChatPrompt] = useState(''); const [pendingSkill, setPendingSkill] = useState(null); const [pendingTask, setPendingTask] = useState(null); const userPickedAgentRef = useRef(false); // newRecord: when set, the record view is rendered in its empty-state form // (title editable, single "New output" chip, no artifact yet). null when // viewing an existing record. const [newRecord, setNewRecord] = useState(null); // null | { name } // Dashboard-generation simulation (triggered from the "Create a project // dashboard from hub knowledge base" prompt in the empty-record state). const [genActive, setGenActive] = useState(false); const [genStageIdx, setGenStageIdx] = useState(0); // True once the agent has finished generating the dashboard for the current // record session. Resets when the user navigates away or starts a fresh // record. Drives the "single Dashboard chip, 0 comments, no source updates" // appearance and lets the "+" on the output row create new outputs. const [agentGenerated, setAgentGenerated] = useState(false); // True once a template has been inserted into the record as an editable // artifact via the templates empty-state "Create empty" action. const [templateArtifact, setTemplateArtifact] = useState(false); const genTimersRef = useRef([]); const [isMobile, setIsMobile] = useState(typeof window !== 'undefined' && window.matchMedia('(max-width: 1024px)').matches); useEffect(() => { const mq = window.matchMedia('(max-width: 1024px)'); const onChange = () => setIsMobile(mq.matches); mq.addEventListener('change', onChange); return () => mq.removeEventListener('change', onChange); }, []); // Auto-switch chat agent based on the current view (until user manually picks). useEffect(() => { if (userPickedAgentRef.current) return; if (view === 'record') { setChatAgent('node'); } else { setChatAgent('knowledge'); } }, [view]); // Clear new-record state when navigating away from the record view useEffect(() => { if (view !== 'record' && newRecord) setNewRecord(null); if (view !== 'record' && genActive) { clearGenTimers(); setGenActive(false); setGenStageIdx(0); } if (view !== 'record' && agentGenerated) setAgentGenerated(false); if (view !== 'record' && templateArtifact) setTemplateArtifact(false); }, [view]); // Triggered from the "+" button on any of the file tables const handleCreateRecord = () => { setNewRecord({ name: 'New Record' }); setAgentGenerated(false); setTemplateArtifact(false); userPickedAgentRef.current = false; // let auto-switch flip to Node Agent setView('record'); }; // Triggered from the empty-state "AI Agent" card (with a prompt → force the // Data Product agent) or the header Agent bubble (bare → Knowledge Base). const handleOpenAgentChat = (text) => { const hasPrompt = typeof text === 'string' && text.trim(); setHubMenuOpen(false); setChatAgent(hasPrompt ? 'node' : 'knowledge'); userPickedAgentRef.current = true; setChatOpen(true); if (hasPrompt) { setPendingChatPrompt(text.trim()); } // Special path: dashboard-generation prompt → simulate the agent building // the Cedar Narrows WST Project Overview dashboard. Stage labels match what the // chat panel streams, so the placeholder in the record area and the // assistant "thinking" line in chat stay in sync. if (typeof text === 'string' && text.trim() === window.DASHBOARD_GEN_PROMPT) { startDashboardGeneration(); } }; // Empty-workspace "Add …" CTAs (Hubs/Team/Org/Context/About) open the agent // with a walkthrough for that section — the same guided pattern as picking a // template, just starting from nothing instead of a known shape. const WS_GUIDE_MSGS = { hub: [ `Let's build your first hub. Tell me what you would like to model — or upload a website, spreadsheet, report or docs — and I'll propose the hub and its `, { bold: 'data structure' }, ` so we can then iterate.`, { br: true }, { br: true }, `Or pick one of the `, { bold: 'Templates' }, ` rather than starting from scratch.`, ], team: [ `Let's add your team. Paste names and emails — or upload a roster, a directory export or a paper's author list — and I'll set up members with suggested roles (`, { bold: 'Owner' }, `, `, { bold: 'Admin' }, `, `, { bold: 'Editor' }, `, `, { bold: 'Viewer' }, `) so we can then iterate.`, ], org: [ `Let's draft your structure. Upload a website, a slide, a sheet or even a photo of a napkin sketch — I'll extract the groups, leads and reporting lines into a chart we can iterate on.`, ], context: [ `Let's capture shared context — the judgment your data can't hold. Tell me the ground rules every hub should honor (definitions, conventions, priorities) and I'll organize them as context items for you to `, { bold: 'validate' }, `.`, ], about: [ `Let's write your About. Give me a sentence or two on what this workspace models and who it serves — I'll draft a short doc anyone new can read, and we can iterate from there.`, ], }; const guideWorkspaceAdd = (kind) => { setHubMenuOpen(false); setChatAgent('knowledge'); userPickedAgentRef.current = true; setAssistantWide(false); setOnboardMsg(WS_GUIDE_MSGS[kind] || WS_GUIDE_MSGS.hub); setChatOpen(true); }; // Clicking a Planned/Template hub asks the assistant to build it. const handleProposeHub = (hubName) => { setHubMenuOpen(false); setChatAgent('knowledge'); userPickedAgentRef.current = true; setAssistantWide(false); setOnboardMsg([ `Would you like me to create the `, { bold: `${hubName} hub` }, `?`, { br: true }, { br: true }, `I'll set it up from your model — pulling in the relevant groups, owners and sources — then propose its data structure so we can iterate.`, ]); setChatOpen(true); }; const clearGenTimers = () => { genTimersRef.current.forEach((id) => clearTimeout(id)); genTimersRef.current = []; }; const startDashboardGeneration = () => { clearGenTimers(); setGenActive(true); setGenStageIdx(0); setTemplateArtifact(false); const stages = window.DASHBOARD_GEN_STAGES || []; // Stage timing: most stages ~1.3s, the "composing" final stage a touch // longer so it feels deliberate. Total budget ≈ 9s. const stageDurations = [1100, 1300, 1400, 1500, 1400, 1700]; let elapsed = 0; stages.forEach((_, i) => { if (i === 0) return; // first stage shown immediately elapsed += stageDurations[i - 1] || 1300; const id = setTimeout(() => setGenStageIdx(i), elapsed); genTimersRef.current.push(id); }); // Final transition: drop newRecord so the dashboard renders, name + output // chip switch automatically (record name is hardcoded on the non-empty // header, output defaults to 'dashboard'). Mark the record as agent- // generated so the chrome (output chips, comments count, source updates // indicator) reflects a brand-new artifact. elapsed += stageDurations[stages.length - 1] || 1500; const finishId = setTimeout(() => { setGenActive(false); setNewRecord(null); setGenStageIdx(0); setAgentGenerated(true); }, elapsed); genTimersRef.current.push(finishId); }; useEffect(() => () => clearGenTimers(), []); // Expose a global so the Control tab's Skills table (plain-HTML, injected via // hub_tab_content.js) can open the chat with a /skill mention pre-inserted in // the composer — mirroring how typing "/" picks a skill. useEffect(() => { window.hubOpenSkill = (name, mode, id) => { setChatAgent('knowledge'); userPickedAgentRef.current = true; setChatOpen(true); setPendingSkill({ name: name, mode: mode || 'use', id: id || '', nonce: Date.now() }); }; return () => { delete window.hubOpenSkill; }; }, []); // Control · Tasks: the plain-HTML task table (control_sections.js) calls this // on a row click to open the chat and run that task's scripted flow. useEffect(() => { window.hubOpenTask = (id) => { setChatAgent('knowledge'); userPickedAgentRef.current = true; setChatOpen(true); setPendingTask({ id: id, nonce: Date.now() }); }; return () => { delete window.hubOpenTask; }; }, []); // Template flow: "+ Add → upload files" in the preview modal → reuse the // agent dashboard-generation loader, ending on the real Cedar Narrows dashboard. const handleTemplateGenerate = () => { startDashboardGeneration(); }; // Node-agent chat prompt ("Build a mobile dashboard…") fired from within an // open empty record → build the Cedar Narrows dashboard from the hub's knowledge // base. Returns true if generation was started (i.e. a record is open) so the // chat panel can pick the matching "generating" response. const handleAgentDashboardGen = () => { if (!newRecord) return false; setAssistantWide(true); // generated dashboard needs the larger canvas startDashboardGeneration(); return true; }; // Template flow: "Create empty" → insert the template as an editable artifact // into the (still empty) record shell, naming the record after the template. const handleTemplateCreateEmpty = (tpl) => { setTemplateArtifact(true); if (tpl && tpl.title) setNewRecord((r) => (r ? { ...r, name: tpl.title } : { name: tpl.title })); }; const handleAgentChange = (next) => { userPickedAgentRef.current = true; setChatAgent(next); }; const pickHub = (h) => { setActiveHub(h); setView('project'); setHubMenuOpen(false); }; const handleOpenHubMenu = () => { setChatOpen(false); setAssistantWide(false); setHubMenuOpen((o) => !o); }; const railProps = { expanded: railExpanded, onToggleExpand: () => setRailExpanded((o) => !o), activeHub, onPickHub: setActiveHub, onOpenGraph: () => setView('graph'), graphOpen: view === 'graph', onOpenAbout: () => setView('about'), aboutOpen: view === 'about', onGoHome: () => setView('project'), onOpenWorkspace: () => setView('workspace'), workspaceOpen: view === 'workspace', createdHubs: activeDrafts, onCreateHub: createHub, // Rail sections follow the active workspace: Cascade Ridge's are the // module-level WORKSPACES; a built domain pack (the lab) brings its own. groups: wsIsEmpty ? [] : ((domPack && domPack.built && domPack.built.rail) || WORKSPACES), workspaceName: curWorkspace.name, mark: window.proxaMark ? window.proxaMark(activeDomain) : null, onOpenLanding: () => setView('landing'), }; const openPhoto = (from) => (id, tab) => { setPhotoId(id); setPhotoFrom(from); if (from === 'collection' && tab) setCollectionTab(tab); setView('photo'); }; // Citation click → navigate the main column to the cited record/collection const openCite = (cite) => { if (!cite) return; if (cite.target === 'record') { setView('record'); } else if (cite.target === 'collection') { if (cite.tab) setCollectionTab(cite.tab); setView('collection'); } }; // CSS vars on the app shell drive the rail + chat column widths. const docked = assistantMode === 'dock'; const beneathOpen = chatOpen && !isMobile && !docked; const appStyle = { '--rail-width': isMobile ? '0px' : (railExpanded ? '264px' : '68px'), // Beneath mode reveals via depth, not a reserved column, so keep chat-width 0. '--chat-width': (chatOpen && !isMobile && docked) ? `${chatWidth}px` : '0px', // Fraction of the content area the page card keeps: wider page = narrower // surface (0.74) by default; expanded surface leaves the page at 0.42. '--peel-frac': assistantWide ? 0.42 : 0.74, }; // NOTE: the beneath surface is a persistent, narrow surface you interact with // alongside the page — so the page must stay fully clickable. (Earlier a // click-on-page-to-close handler lived here; it swallowed page clicks like the // upload button, so it was removed. Close the assistant via its X.) const renderInner = () => { if (view === 'workspace') { return { setActiveHub(h); setView('project'); }} onOpenSettings={() => { setSettingsScope('workspace'); setSettingsOpen(true); }} onProposeHub={handleProposeHub} onOpenAgentChat={handleOpenAgentChat} workspaces={workspaces} activeWorkspaceId={activeWorkspace} onSwitchWorkspace={switchWorkspace} onCreateWorkspace={createWorkspace} onRemoveWorkspace={removeWorkspace} onGuide={guideWorkspaceAdd} draftHubs={activeDrafts} onBuildFromTemplate={buildFromTemplate} onCreateHub={createHub} domain={activeDomain} onPickDomain={pickDomain} domainOf={(id) => wsDomains[id]} />; } if (view === 'graph') { return (
setView('workspace')} domain={activeDomain} />
); } if (view === 'about') { return setView('workspace')} domain={activeDomain} />; } if (view === 'colgraph') { return (
setView('collection')} />
); } if (view === 'photo') { return (
setView(photoFrom)} />
); } if (view === 'record') { return (
setView('collection')} onOpenPhoto={openPhoto('record')} newRecord={newRecord} setNewRecordName={(name) => setNewRecord((r) => r ? { ...r, name } : r)} onOpenAgentChat={handleOpenAgentChat} generating={genActive} genStageIdx={genStageIdx} genStages={window.DASHBOARD_GEN_STAGES} agentGenerated={agentGenerated} onTemplateGenerate={handleTemplateGenerate} onTemplateCreateEmpty={handleTemplateCreateEmpty} templateArtifact={templateArtifact} />
); } if (view === 'collection') { return (
setView('project')} onOpenRecord={() => setView('record')} onCreateRecord={handleCreateRecord} onOpenGraph={() => setView('colgraph')} initialTab={collectionTab} onTabChange={setCollectionTab} onOpenPhoto={openPhoto('collection')} />
); } // New Hub workflow: a freshly created hub renders through the real hub shell // (HubHome) as an EMPTY state — standard banner + tiles, no content — with the // template picker living in the empty Artifacts tab. Diligence keeps its own // full-screen upload step once picked. const flow = hubFlow[activeHub]; // Create-from-scratch: brand-new hub (no template chosen) → "What should we build?" if (flow && flow.step === 'template') { return ; } if (flow && flow.template === 'diligence' && flow.step !== 'built') { return ; } return ( { setSettingsScope('hub'); setSettingsOpen(true); }} onOpenAgentChat={handleOpenAgentChat} domain={activeDomain} /> ); }; const screenLabels = { graph: 'Context Graph', about: 'About — Proxa Model', colgraph: 'Cedar Narrows Collection — Context Graph', photo: 'Cedar Narrows Photo', record: 'Cedar Narrows Record', collection: 'Cedar Narrows Collection', project: 'Cedar Narrows Hub', }; // The landing page is the POC's front door — rendered without the app shell // (no rail, chat or tab bar). Both paths land on the Workspace page. if (view === 'landing') { return ( { setActiveWorkspace('cascade'); setView('workspace'); }} onLab={() => { setActiveWorkspace('lab'); setView('workspace'); }} onScratch={() => createWorkspace()} /> ); } return (
setAssistantWide((w) => !w)} onboardMessage={onboardMsg} onOnboardMessageHandled={() => setOnboardMsg(null)} agent={chatAgent} setAgent={handleAgentChange} onClose={() => { setChatOpen(false); setAssistantWide(false); }} onWidthChange={setChatWidth} onOpenCite={openCite} initialPrompt={pendingChatPrompt} onGenerateDashboard={handleAgentDashboardGen} pendingSkill={pendingSkill} onPendingSkillHandled={() => setPendingSkill(null)} pendingTask={pendingTask} onPendingTaskHandled={() => setPendingTask(null)} onInitialPromptHandled={() => setPendingChatPrompt('')} /> {renderInner()} setSettingsOpen(false)} /> setHubMenuOpen(false)} activeHub={activeHub} onPickHub={pickHub} onCreateHub={createHub} createdHubs={createdHubs} /> setHubMenuOpen(false)} hubMenuOpen={hubMenuOpen} />
); } ReactDOM.createRoot(document.getElementById('root')).render();