// Research Hub + Thought Leadership (wired to PrimaAPI)
const ResearchHub = ({ showRbac }) => {
const [cat, setCat] = React.useState("all");
const [items, setItems] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [showAdd, setShowAdd] = React.useState(false);
const loadItems = () => {
setLoading(true);
PrimaAPI.listResearch()
.then(data => setItems(data || []))
.catch(() => setItems([]))
.finally(() => setLoading(false));
};
React.useEffect(() => { loadItems(); }, []);
// Map API fields to display format
const display = items.map(r => ({
id: r.id,
title: r.title,
source: r.source,
category: r.category,
scope: r.geo_scope || [],
industries: r.industry_tags || [],
date: r.publish_date || "—",
conf: r.confidentiality || "internal",
}));
const filtered = display.filter(r => cat === "all" || r.category === cat);
return (
External intelligence
Research Hub
Whitepapers, market scans, competitor benchmarks, and regulatory notes from external sources — tagged for downstream retrieval.
{["all", ...TAXONOMY.researchCats].map(c => (
))}
Sources:
{TAXONOMY.publishers.slice(0, 4).map(p => {p})}
{loading ? (
) : filtered.length === 0 ? (
No research items yet
Add external intelligence to build the firm's knowledge base.
) : (
{filtered.map((r, i) => (
{r.source} · {r.date}
{r.title}
{r.industries.join(", ")} · {r.scope.join(", ")}
{r.category}
{r.industries.map(ind => {ind})}
{r.scope.map(s => {s})}
))}
)}
setShowAdd(false)} onSaved={() => { loadItems(); setShowAdd(false); }} />
);
};
// ─── Add Research Modal ───
const AddResearchModal = ({ open, onClose, onSaved }) => {
const [form, setForm] = React.useState({
title: "", source: "", category: "", geo_scope: [], industry_tags: [], publish_date: "", confidentiality: "internal"
});
const [file, setFile] = React.useState(null);
const [errors, setErrors] = React.useState({});
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (open) {
setForm({ title: "", source: "", category: "", geo_scope: [], industry_tags: [], publish_date: "", confidentiality: "internal" });
setFile(null);
setErrors({});
}
}, [open]);
const validate = () => {
const e = {};
if (!form.title.trim()) e.title = "Required.";
if (!form.source.trim()) e.source = "Required.";
if (!form.category) e.category = "Required.";
setErrors(e);
return Object.keys(e).length === 0;
};
const submit = async () => {
if (!validate()) return;
setLoading(true);
try {
let gcs_path = "";
if (file) {
const resp = await PrimaAPI.uploadHubFile("research", file);
gcs_path = resp.gcs_path || "";
}
await PrimaAPI.addResearch({ ...form, gcs_path });
onSaved();
} catch (e) {
setErrors({ api: e.message || "Failed to add research item." });
} finally {
setLoading(false);
}
};
return (
>}>
{errors.api && (
{errors.api}
)}
setForm({ ...form, title: e.target.value })} placeholder="e.g. State of Urban Mobility 2026" error={errors.title} autoFocus />
setForm({ ...form, geo_scope: v })} options={TAXONOMY.regions} />
setForm({ ...form, industry_tags: v })} options={TAXONOMY.industries} />
setForm({ ...form, publish_date: e.target.value })} placeholder="e.g. Apr 2026" />
{file && }
);
};
const ThoughtLeadership = () => {
const [type, setType] = React.useState("all");
const [items, setItems] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [showAdd, setShowAdd] = React.useState(false);
const loadItems = () => {
setLoading(true);
PrimaAPI.listThought()
.then(data => setItems(data || []))
.catch(() => setItems([]))
.finally(() => setLoading(false));
};
React.useEffect(() => { loadItems(); }, []);
// Map API fields to display format
const display = items.map(p => ({
id: p.id,
title: p.title,
type: p.content_type,
domain: p.primary_domain || "—",
themes: p.themes || [],
authors: p.authors || [],
date: p.publish_date || "—",
conf: p.confidentiality || "internal",
}));
const filtered = display.filter(p => type === "all" || p.type === type);
return (
Internal POVs
Thought Leadership
Articles, op-eds, and whitepapers authored by Primus consultants — the firm's external voice, organised by domain and theme.
{["all", ...TAXONOMY.contentTypes].map(c => (
))}
{loading ? (
) : filtered.length === 0 ? (
No thought leadership items yet
Publish internal POVs to build the firm's voice.
) : (
{filtered.map((p, i) => (
{p.type}
{p.domain}
{p.date}
{p.title}
{p.authors.map((a, ai) => (
))}
{p.authors.join(" · ")}
{p.themes.map(t => {t})}
))}
)}
setShowAdd(false)} onSaved={() => { loadItems(); setShowAdd(false); }} />
);
};
// ─── Add Thought Leadership Modal ───
const AddThoughtModal = ({ open, onClose, onSaved }) => {
const [form, setForm] = React.useState({
title: "", content_type: "", primary_domain: "", themes: [], authors: [], publish_date: "", confidentiality: "internal"
});
const [file, setFile] = React.useState(null);
const [errors, setErrors] = React.useState({});
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (open) {
setForm({ title: "", content_type: "", primary_domain: "", themes: [], authors: [], publish_date: "", confidentiality: "internal" });
setFile(null);
setErrors({});
}
}, [open]);
const validate = () => {
const e = {};
if (!form.title.trim()) e.title = "Required.";
if (!form.content_type) e.content_type = "Required.";
setErrors(e);
return Object.keys(e).length === 0;
};
const submit = async () => {
if (!validate()) return;
setLoading(true);
try {
let gcs_path = "";
if (file) {
const resp = await PrimaAPI.uploadHubFile("thought-leadership", file);
gcs_path = resp.gcs_path || "";
}
await PrimaAPI.addThought({ ...form, gcs_path });
onSaved();
} catch (e) {
setErrors({ api: e.message || "Failed to publish POV." });
} finally {
setLoading(false);
}
};
return (
>}>
{errors.api && (
{errors.api}
)}
setForm({ ...form, title: e.target.value })} placeholder="e.g. Why citizen-experience must lead digital-government roadmaps" error={errors.title} autoFocus />
setForm({ ...form, themes: v })} options={["Generative AI", "Agentic AI", "Urbanization", "Climate", "Disclosure", "Citizen Services", "Programme Mgmt", "Banking", "Health"]} allowCustom />
setForm({ ...form, authors: v })} options={[]} allowCustom />
setForm({ ...form, publish_date: e.target.value })} placeholder="e.g. Apr 24, 2026" />
{file && }
);
};
Object.assign(window, { ResearchHub, ThoughtLeadership, AddResearchModal, AddThoughtModal });