Skip to content

Commit 8871317

Browse files
Merge pull request #3850 from verifywise-ai/mo-344-may-5-core-governance-os
Core Governance OS
2 parents ba7c272 + 9f6c952 commit 8871317

48 files changed

Lines changed: 5629 additions & 83 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Clients/src/App.tsx

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ import {
3939
} from "./presentation/components/UserGuide";
4040
import { AdvisorConversationProvider } from "./application/contexts/AdvisorConversation.context";
4141
import { PluginRegistryProvider } from "./application/contexts/PluginRegistry.context";
42+
import { SmartPromptProvider } from "./application/contexts/SmartPrompt.context";
4243
import PluginLoader from "./presentation/components/PluginLoader";
44+
import SmartPrompt from "./presentation/components/SmartPrompt";
4345
// SSE notifications disabled for now - can be re-enabled later if needed
4446
// import { useNotifications } from "./application/hooks/useNotifications";
4547

@@ -262,34 +264,37 @@ function App() {
262264
<PluginRegistryProvider>
263265
<PluginLoader />
264266
<UserGuideSidebarProvider>
265-
<ConditionalThemeWrapper>
266-
{alert && (
267-
<Alert
268-
variant={alert.variant}
269-
title={alert.title}
270-
body={alert.body}
271-
isToast={true}
272-
onClick={() => setAlert(null)}
273-
/>
274-
)}
275-
<CommandPaletteErrorBoundary>
276-
<CommandPalette
277-
open={commandPalette.isOpen}
278-
onOpenChange={commandPalette.close}
279-
/>
280-
</CommandPaletteErrorBoundary>
281-
{showModal && (
282-
<SetupModal onComplete={handleOnboardingDone} onSkip={handleOnboardingDone} />
283-
)}
284-
<ChunkErrorBoundary>
285-
<Routes>{createRoutes(triggerSidebar, triggerSidebarReload)}</Routes>
286-
</ChunkErrorBoundary>
267+
<SmartPromptProvider>
268+
<ConditionalThemeWrapper>
269+
{alert && (
270+
<Alert
271+
variant={alert.variant}
272+
title={alert.title}
273+
body={alert.body}
274+
isToast={true}
275+
onClick={() => setAlert(null)}
276+
/>
277+
)}
278+
<SmartPrompt />
279+
<CommandPaletteErrorBoundary>
280+
<CommandPalette
281+
open={commandPalette.isOpen}
282+
onOpenChange={commandPalette.close}
283+
/>
284+
</CommandPaletteErrorBoundary>
285+
{showModal && (
286+
<SetupModal onComplete={handleOnboardingDone} onSkip={handleOnboardingDone} />
287+
)}
288+
<ChunkErrorBoundary>
289+
<Routes>{createRoutes(triggerSidebar, triggerSidebarReload)}</Routes>
290+
</ChunkErrorBoundary>
287291

288-
{/* User Guide Sidebar with Advisor Conversation persistence */}
289-
<AdvisorConversationProvider>
290-
<UserGuideSidebarContainer />
291-
</AdvisorConversationProvider>
292-
</ConditionalThemeWrapper>
292+
{/* User Guide Sidebar with Advisor Conversation persistence */}
293+
<AdvisorConversationProvider>
294+
<UserGuideSidebarContainer />
295+
</AdvisorConversationProvider>
296+
</ConditionalThemeWrapper>
297+
</SmartPromptProvider>
293298
</UserGuideSidebarProvider>
294299
</PluginRegistryProvider>
295300
</VerifyWiseContext.Provider>

Clients/src/application/config/entityTips.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,28 @@ export const ENTITY_TIPS: EntityTips = {
288288
"Not everyone needs access to all governance data. Use roles to grant appropriate access levels: viewers for stakeholders, editors for governance team members, and admins for system owners. Role-based access protects sensitive information.",
289289
},
290290
],
291+
"governance-os": [
292+
{
293+
header: "Cross-framework mappings reveal shared compliance effort.",
294+
content:
295+
"Many controls in different frameworks (EU AI Act, ISO 42001, NIST AI RMF) overlap. The Framework Mapper shows these overlaps so you can satisfy multiple requirements with a single implementation, saving time and reducing duplication.",
296+
},
297+
{
298+
header: "Scenario recommendations match frameworks to your context.",
299+
content:
300+
"The Scenario Builder uses your industry, region, risk level, and use case type to recommend which frameworks to prioritize. Select a scenario to set your organization's governance strategy and guide compliance planning.",
301+
},
302+
{
303+
header: "Coverage analysis identifies gaps and synergies per project.",
304+
content:
305+
"Unified Insights shows how well each project covers its assigned frameworks. Gaps highlight controls that still need implementation, while synergies show controls that satisfy multiple frameworks at once.",
306+
},
307+
{
308+
header: "Governance domains group controls by topic area.",
309+
content:
310+
"Controls are tagged with domains like data governance, risk management, or transparency. Filter by domain to focus on a specific governance area across all frameworks simultaneously.",
311+
},
312+
],
291313
"shadow-ai-insights": [
292314
{
293315
header: "Shadow AI insights reveal hidden AI usage across your organization.",

Clients/src/application/config/routes.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ const MCPGuardrailsPage = lazyRoute(
130130
() => import("../../presentation/pages/AIGateway/MCPGuardrails"),
131131
);
132132

133+
// ── Governance OS routes ─────────────────────────────────────────────
134+
const GovernanceOS = lazyRoute(() => import("../../presentation/pages/GovernanceOS"));
135+
133136
// ── Remaining routes ──────────────────────────────────────────────────
134137
const Plugins = lazyRoute(() => import("../../presentation/pages/Plugins"));
135138
const PluginManagement = lazyRoute(
@@ -337,6 +340,14 @@ export const createRoutes = (
337340
</Suspense>
338341
}
339342
/>
343+
<Route
344+
path="/governance-os/:tab?"
345+
element={
346+
<Suspense fallback={<LazyFallback />}>
347+
<GovernanceOS />
348+
</Suspense>
349+
}
350+
/>
340351
<Route
341352
path="/project-view"
342353
element={
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from "react";
2+
3+
export interface SmartPromptConfig {
4+
id: string;
5+
type: string;
6+
title: string;
7+
message: string;
8+
primaryAction?: {
9+
label: string;
10+
onClick: () => void;
11+
};
12+
secondaryAction?: {
13+
label: string;
14+
onClick: () => void;
15+
};
16+
dontAskAgainKey?: string;
17+
onDontAskAgain?: (key: string) => void;
18+
autoDismissMs?: number;
19+
}
20+
21+
interface SmartPromptContextValue {
22+
showPrompt: (config: Omit<SmartPromptConfig, "id">) => void;
23+
dismissPrompt: (id: string) => void;
24+
activePrompt: SmartPromptConfig | null;
25+
hasDontAskAgain: (key: string) => boolean;
26+
setDontAskAgain: (key: string, value: boolean) => void;
27+
}
28+
29+
const SmartPromptContext = createContext<SmartPromptContextValue | null>(null);
30+
31+
const DONT_ASK_AGAIN_PREFIX = "vw_smart_prompt_dont_ask_";
32+
33+
export const SmartPromptProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
34+
const [queue, setQueue] = useState<SmartPromptConfig[]>([]);
35+
const [activePrompt, setActivePrompt] = useState<SmartPromptConfig | null>(null);
36+
const idCounter = useRef(0);
37+
38+
const hasDontAskAgain = useCallback((key: string) => {
39+
try {
40+
return localStorage.getItem(`${DONT_ASK_AGAIN_PREFIX}${key}`) === "true";
41+
} catch {
42+
return false;
43+
}
44+
}, []);
45+
46+
const setDontAskAgain = useCallback((key: string, value: boolean) => {
47+
try {
48+
if (value) {
49+
localStorage.setItem(`${DONT_ASK_AGAIN_PREFIX}${key}`, "true");
50+
} else {
51+
localStorage.removeItem(`${DONT_ASK_AGAIN_PREFIX}${key}`);
52+
}
53+
} catch {
54+
// ignore localStorage errors
55+
}
56+
}, []);
57+
58+
const showPrompt = useCallback(
59+
(config: Omit<SmartPromptConfig, "id">) => {
60+
if (config.dontAskAgainKey && hasDontAskAgain(config.dontAskAgainKey)) {
61+
return;
62+
}
63+
64+
const newPrompt: SmartPromptConfig = {
65+
...config,
66+
id: `prompt-${Date.now()}-${++idCounter.current}`,
67+
};
68+
69+
setQueue((prev) => {
70+
// dedupe by type
71+
if (prev.some((p) => p.type === newPrompt.type)) {
72+
return prev;
73+
}
74+
return [...prev, newPrompt];
75+
});
76+
},
77+
[hasDontAskAgain],
78+
);
79+
80+
const dismissPrompt = useCallback((id: string) => {
81+
setQueue((prev) => prev.filter((p) => p.id !== id));
82+
setActivePrompt((prev) => (prev?.id === id ? null : prev));
83+
}, []);
84+
85+
// Promote next prompt from queue when active is cleared
86+
useEffect(() => {
87+
if (!activePrompt && queue.length > 0) {
88+
const next = queue[0];
89+
setActivePrompt(next);
90+
setQueue((prev) => prev.slice(1));
91+
}
92+
}, [activePrompt, queue]);
93+
94+
const value: SmartPromptContextValue = {
95+
showPrompt,
96+
dismissPrompt,
97+
activePrompt,
98+
hasDontAskAgain,
99+
setDontAskAgain,
100+
};
101+
102+
return <SmartPromptContext.Provider value={value}>{children}</SmartPromptContext.Provider>;
103+
};
104+
105+
export const useSmartPromptContext = () => {
106+
const ctx = useContext(SmartPromptContext);
107+
if (!ctx) {
108+
throw new Error("useSmartPromptContext must be used within SmartPromptProvider");
109+
}
110+
return ctx;
111+
};

0 commit comments

Comments
 (0)