// Record side panels — Details, Comments, Data Lineage, Version History.
// Opened from the record context (3-dots) menu. Layouts mirror the macro
// mockups in uploads/pasted-1779091716223-0.png.
const { useState: useStateRP, useEffect: useEffectRP, useRef: useRefRP, createContext: createContextRP, useContext: useContextRP } = React;
/* ===== Output context — current output (Dashboard / Report / Slide) =====
Created here so both the record body (collection.jsx) and the side panels
(this file) can read/write it without prop-drilling. Side panels use it to
default their per-output filter to whatever output is currently visible. */
const OutputContext = createContextRP(null);
function OutputProvider({ children, newRecord, setNewRecordName, onOpenAgentChat, generating, genStageIdx, genStages, agentGenerated, onTemplateGenerate, onTemplateCreateEmpty, templateArtifact }) {
const [output, setOutput] = useStateRP('dashboard'); // current active output key
const [openPanel, setOpenPanel] = useStateRP(null); // 'details' | 'comments' | 'lineage' | 'history' | null
// Extra outputs the user adds via the "+" on the chip row. Each entry:
// { key, label, state: 'empty' | 'done' }. Only meaningful when the record
// has already been generated (agentGenerated=true).
const [extraOutputs, setExtraOutputs] = useStateRP([]);
const nextIdRef = useRefRP(1);
// Reset extras whenever the record context resets (e.g. user navigates away
// and creates a brand new record).
useEffectRP(() => {
if (!agentGenerated) {
setExtraOutputs([]);
nextIdRef.current = 1;
}
}, [agentGenerated]);
const addOutput = () => {
const id = nextIdRef.current++;
const key = `new-${id}`;
setExtraOutputs((arr) => {
// First extra is just "New output"; subsequent ones get an index suffix
// so chips remain distinguishable.
const label = arr.length === 0 ? 'New output' : `New output ${arr.length + 1}`;
return [...arr, { key, label, state: 'empty' }];
});
setOutput(key);
};
const setOutputState = (key, state) => {
setExtraOutputs((arr) => arr.map((o) => (o.key === key ? { ...o, state } : o)));
};
const removeOutput = (key) => {
setExtraOutputs((arr) => arr.filter((o) => o.key !== key));
setOutput('dashboard');
};
return (
{children}
);
}
function useOutput() {
return useContextRP(OutputContext);
}
/* Pretty labels for filter dropdowns */
const OUTPUT_LABELS = { dashboard: 'Dashboard', report: 'Report', slide: 'Slide' };
const OUTPUT_FILTER_OPTIONS = ['All outputs', 'Dashboard', 'Report', 'Slide'];
const labelFromKey = (k) => OUTPUT_LABELS[k] || k;
const keyFromLabel = (l) => Object.keys(OUTPUT_LABELS).find((k) => OUTPUT_LABELS[k] === l) || null;
/* ===== Shared shell ===== */
function PanelShell({ title, onClose, toolbar, footer, children, bodyScrolls = true, className = '' }) {
// Lock scroll while open
useEffectRP(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, []);
return (
{toolbar && {toolbar}
}
{children}
{footer && {footer}
}
);
}
/* ===== Simple select pill (display-only, click-through is decorative) ===== */
function FilterSelect({ value, onChange, options }) {
const [open, setOpen] = useStateRP(false);
const wrapRef = useRefRP(null);
useEffectRP(() => {
if (!open) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
return (
setOpen((o) => !o)}
aria-haspopup="listbox"
aria-expanded={open}>
{value}
{open && (
{options.map((o) => (
{ onChange(o); setOpen(false); }}>
{o}
))}
)}
);
}
/* =========================================================================
1. DETAILS PANEL
========================================================================= */
function EditableProp({ value, options, onChange }) {
const [open, setOpen] = useStateRP(false);
const wrapRef = useRefRP(null);
useEffectRP(() => {
if (!open) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
};
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [open]);
return (
setOpen((o) => !o)}>
{value}
{open && (
{options.map((o) => (
{ onChange(o); setOpen(false); }}>
{o}
))}
)}
);
}
const DETAILS_USED_IN = [
{ t: 'Finance reporting', sub: 'Q1 2026 dashboards' },
{ t: 'MV Owner submissions', sub: 'Monthly progress packets' },
{ t: 'TAGP GP meetings', sub: 'Meeting #13 – #14 decks' },
{ t: 'Executive summaries', sub: 'AWST Q1 2026 brief' },
];
function RecordDetailsPanel({ onClose }) {
const ctx = useOutput();
const isEmpty = ctx?.isEmpty;
const recordName = ctx?.newRecord?.name || 'Untitled record';
const [descOpen, setDescOpen] = useStateRP(true);
const [propsOpen, setPropsOpen] = useStateRP(true);
const [usedInOpen, setUsedInOpen] = useStateRP(true);
const [status, setStatus] = useStateRP(isEmpty ? 'Draft' : 'Need review');
const [priority, setPriority] = useStateRP('Medium');
const [kb, setKb] = useStateRP(isEmpty ? 'Pending review' : 'Included');
return (
setDescOpen((o) => !o)} aria-expanded={descOpen}>
Description
{descOpen && (
{isEmpty ? (
No description yet. Add one once the agent or a template has filled this record with a first artifact.
) : (
Aggregated dashboard of the Cedar Narrows Water Supply Tunnel project (Fraser River Crossing, Metro Vancouver
Contract RFP No. 20-354). Synthesises the latest monthly, weekly, and daily shift reports — safety
metrics, pipe installation progress, active workfronts, manpower, and open commercial items — into a
single live view for the TAGP team and the Owner.
)}
)}
setPropsOpen((o) => !o)} aria-expanded={propsOpen}>
Properties
{propsOpen && (
{isEmpty ? (
Name {recordName}
Owner James Anderson
Team Marketing Team
Status
Priority
Knowledge Base
Location Proxa / Traylor / Cedar Narrows WST
Skills used —
Created Just now · by James Anderson
Last updated Just now
) : (
Owner James Anderson
Team TAGP — Project Controls
Status
Priority
Knowledge Base
Location Proxa / Traylor / Cedar Narrows WST
Skills used report-aggregator (v1.04)
construction-kpi-extractor (v1.02)
Created January 20, 2022 · 09:15
Last updated 4 min ago by Vera Hanna
)}
)}
setUsedInOpen((o) => !o)} aria-expanded={usedInOpen}>
Used in
{usedInOpen && (
{isEmpty ? (
Not used anywhere yet
Once this record has an artifact, places that reference it will show up here.
) : (
)}
)}
);
}
/* =========================================================================
2. COMMENTS PANEL
========================================================================= */
const RP_COMMENTS = [
{ id: 'c1', who: 'Lorri Warf', ago: '1 day ago', output: 'dashboard', avatar: 'LW',
body: 'Forecast Substantial Completion at Nov 8, 2027 — please verify against the Rev.08 baseline before sending to Metro Vancouver. The positive variance vs. CO-14 SC is worth calling out.' },
{ id: 'c2', who: 'Oliver Smith', ago: '3 days ago', output: 'dashboard', avatar: 'OS',
body: 'Cellular grout backfill is sitting at 4.5%. Confirm with Joe before publishing the next monthly that the slag-mix throughput projection still holds for April.' },
{ id: 'c3', who: 'Mia Chen', ago: '5 days ago', output: 'dashboard', avatar: 'MC',
body: 'SVC dewatering pump test results look good. Adding Well #11 at −18 m is reflected in the workfronts card; archive Wells #1 and #4 once decommissioned.' },
{ id: 'c4', who: 'Elena Martinez', ago: '6 days ago', output: 'report', avatar: 'EM',
body: 'In the "Financial position" section, please re-word the PCO-008 paragraph — current draft reads as accepting CMC\'s framing. Stick to the partial-payment narrative.' },
{ id: 'c5', who: 'Rina Patel', ago: '1 week ago', output: 'report', avatar: 'RP',
body: 'Hero photo for the report should be the NVC excavation shot, not the tunnel-welding one. Excavation reads better as a "where the project is now" image.' },
{ id: 'c6', who: 'James Anderson', ago: '2 weeks ago', output: 'report', avatar: 'JA',
body: 'Bullet under "Safety performance" — let\'s replace "no lost-time incidents in March" with the YTD figure. Owner cares about cumulative, not monthly.' },
{ id: 'c7', who: 'Oliver Smith', ago: '2 weeks ago', output: 'slide', avatar: 'OS',
body: 'NLTI of 14.3 reads alarming on the slide without a comparison number. Add the 4.8 project-to-date average underneath or drop the metric.' },
{ id: 'c8', who: 'Mia Chen', ago: '3 weeks ago', output: 'slide', avatar: 'MC',
body: 'Workfronts = 7 is fine but the slide is too quiet. Consider swapping "Active workfronts" for "Pipe welded %" — it carries the story better.' },
{ id: 'c9', who: 'Oliver Smith', ago: '4 weeks ago', output: 'dashboard', avatar: 'OS',
body: 'Manpower bar widths feel exaggerated for the smaller crews — consider switching to a log scale or fixing the bar max to the largest crew on site.' },
];
function Avatar({ initials }) {
return {initials} ;
}
function RecordCommentsPanel({ onClose, initialScope }) {
const ctx = useOutput();
const output = ctx?.output || 'dashboard';
const isEmpty = ctx?.isEmpty;
const [scope, setScope] = useStateRP(initialScope || labelFromKey(output));
const [draft, setDraft] = useStateRP('');
const toolbar = isEmpty ? null : (
);
const footer = (
);
if (isEmpty) {
return (
No comments yet
Once you and your team start working on this record, comments and threads will live here.
);
}
const filterKey = keyFromLabel(scope); // null when 'All outputs'
const visible = filterKey ? RP_COMMENTS.filter((c) => c.output === filterKey) : RP_COMMENTS;
return (
{visible.length > 0
? (filterKey ? `${visible.length} on ${labelFromKey(filterKey)}` : `${visible.length} across all outputs`)
: 'No comments yet'}
{visible.map((c) => (
{c.who}
·
{c.ago}
·
{labelFromKey(c.output)}
{c.body}
))}
);
}
/* =========================================================================
3. DATA LINEAGE PANEL — LEGACY. Superseded by src/lineage_panel.jsx,
which loads after this file and overrides window.RecordLineagePanel.
Kept only as reference for the original two-list layout.
========================================================================= */
const RP_SOURCES = [
{ t: '2026-03 Monthly Report-R0.pdf', kind: 'doc', outputs: ['dashboard', 'report', 'slide'] },
{ t: 'AWST_Weekly Report_220_29Mar2026.pdf', kind: 'doc', outputs: ['dashboard', 'report'] },
{ t: 'AWST_Weekly Report_219_22Mar2026.pdf', kind: 'doc', outputs: ['dashboard'] },
{ t: 'AWST_Weekly Report_218_15Mar2026.pdf', kind: 'doc', outputs: ['dashboard'] },
{ t: 'AWST Daily Shift Report 12010 — South.pdf', kind: 'doc', outputs: ['dashboard'] },
{ t: 'Safety-Stats-March-2026.xlsx', kind: 'sheet', outputs: ['dashboard', 'report', 'slide'] },
{ t: 'Cost-Tracker-March-2026.xlsx', kind: 'sheet', outputs: ['dashboard', 'report', 'slide'] },
];
const RP_USED_BY = [
{ t: 'MV-Monthly-Submission-Mar2026.pdf', kind: 'doc', outputs: ['dashboard', 'report'] },
{ t: 'AWST-Q1-2026-Executive-Summary.html', kind: 'doc', outputs: ['report', 'slide'] },
{ t: 'TAGP-GP-Meeting-14-Deck.pptx', kind: 'doc', outputs: ['slide'] },
];
function LineageFileIcon({ kind }) {
if (kind === 'sheet') {
return (
);
}
return (
);
}
function RecordLineagePanel({ onClose }) {
const ctx = useOutput();
const output = ctx?.output || 'dashboard';
const isEmpty = ctx?.isEmpty;
const [scope, setScope] = useStateRP(labelFromKey(output));
if (isEmpty) {
return (
No sources connected yet
As soon as the agent extracts data from your sources, you'll see the upstream files here — and any downstream records that pull from this one.
);
}
const toolbar = (
);
const filterKey = keyFromLabel(scope); // null when 'All outputs'
const visibleSources = filterKey ? RP_SOURCES.filter((s) => s.outputs.includes(filterKey)) : RP_SOURCES;
const visibleUsedBy = filterKey ? RP_USED_BY.filter((s) => s.outputs.includes(filterKey)) : RP_USED_BY;
const selfLabel = filterKey
? `This record · ${labelFromKey(filterKey)} output`
: 'This record · 3 outputs';
const renderRow = (s, i) => (
e.preventDefault()}>
{s.t}
);
return (
Sources {visibleSources.length}
{visibleSources.map(renderRow)}
Cedar Narrows WST — Project Overview
{selfLabel}
Used by {visibleUsedBy.length}
{visibleUsedBy.map(renderRow)}
);
}
/* =========================================================================
4. VERSION HISTORY PANEL
========================================================================= */
const RP_VERSIONS = [
{
month: 'April 2026',
items: [
{ d: 'Apr 21 · 12:05 AM', who: 'James Anderson', output: 'dashboard', current: true },
{ d: 'Apr 20 · 9:03 AM', who: 'James Anderson', output: 'report' },
{ d: 'Apr 19 · 12:05 AM', who: 'James Anderson', output: 'dashboard' },
{ d: 'Apr 18 · 4:21 PM', who: 'Vera Hanna', output: 'slide' },
{ d: 'Apr 17 · 9:03 AM', who: 'James Anderson', output: 'report' },
{ d: 'Apr 15 · 12:05 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
],
},
{
month: 'March 2026',
items: [
{ d: 'Mar 28, 12:05 AM', who: 'James Anderson', output: 'dashboard' },
{ d: 'Mar 22, 2:14 PM', who: 'Vera Hanna', output: 'slide' },
{ d: 'Mar 15, 9:03 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
{ d: 'Mar 12, 11:40 AM', who: 'James Anderson', output: 'report' },
{ d: 'Mar 7, 12:05 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
{ d: 'Mar 3, 12:05 AM', who: 'James Anderson', output: 'report' },
{ d: 'Mar 2, 12:05 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
],
},
{
month: 'February 2026',
items: [
{ d: 'Feb 28, 10:05 AM', who: 'James Anderson', output: 'dashboard' },
{ d: 'Feb 24, 9:03 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
{ d: 'Feb 18, 3:48 PM', who: 'Vera Hanna', output: 'slide' },
{ d: 'Feb 7, 12:05 AM', who: 'Proxa AI auto recalculation', output: 'dashboard', ai: true },
],
},
];
function VersionItemMenu() {
const [open, setOpen] = useStateRP(false);
const wrapRef = useRefRP(null);
useEffectRP(() => {
if (!open) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
return (
{ e.stopPropagation(); setOpen((o) => !o); }}>
{open && (
e.stopPropagation()}>
setOpen(false)}>
View full screen
setOpen(false)}>
Restore this version
)}
);
}
function RecordVersionHistoryPanel({ onClose }) {
const ctx = useOutput();
const output = ctx?.output || 'dashboard';
const isEmpty = ctx?.isEmpty;
const [scope, setScope] = useStateRP(labelFromKey(output));
const [author, setAuthor] = useStateRP('All authors');
const [openMonths, setOpenMonths] = useStateRP({ 'April 2026': true, 'March 2026': true, 'February 2026': true });
if (isEmpty) {
return (
Just now
(Current version)
James Anderson
·
Record created
Each time the agent regenerates an artifact, or someone makes a manual edit, a new version will land here.
);
}
const toggle = (m) => setOpenMonths((s) => ({ ...s, [m]: !s[m] }));
const toolbar = (
);
const filterKey = keyFromLabel(scope); // null when 'All outputs'
const filteredGroups = RP_VERSIONS
.map((g) => ({
...g,
items: g.items.filter((it) =>
(!filterKey || it.output === filterKey) &&
(author === 'All authors' || it.who === author)
),
}))
.filter((g) => g.items.length > 0);
return (
{filteredGroups.length === 0 && (
No versions match the current filters.
)}
{filteredGroups.map((group) => (
toggle(group.month)}
aria-expanded={openMonths[group.month]}>
{group.month}
{group.items.length}
{openMonths[group.month] && (
{group.items.map((it, i) => (
{it.d}
{it.current && (Current version) }
{it.ai ? (
) : (
)}
{it.who}
·
{labelFromKey(it.output)}
))}
)}
))}
);
}
Object.assign(window, {
RecordDetailsPanel,
RecordCommentsPanel,
RecordLineagePanel, // legacy — overridden by src/lineage_panel.jsx
RecordVersionHistoryPanel,
OutputProvider,
useOutput,
OUTPUT_LABELS,
RP_COMMENTS_COUNT: RP_COMMENTS.length,
// Shared building blocks for src/lineage_panel.jsx
PanelShell,
FilterSelect,
OUTPUT_FILTER_OPTIONS,
labelFromKey,
keyFromLabel,
});