Forced merge: merge ai-dev into master
This commit is contained in:
commit
64370f5a7c
@ -283,7 +283,7 @@ function config() {
|
||||
projectId,
|
||||
projectUuid: process.env.PROJECT_UUID || null,
|
||||
projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid",
|
||||
defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini",
|
||||
defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5.2-codex",
|
||||
timeout,
|
||||
verifyTls,
|
||||
};
|
||||
|
||||
493
frontend/src/components/AiGovernanceCopilot.tsx
Normal file
493
frontend/src/components/AiGovernanceCopilot.tsx
Normal file
@ -0,0 +1,493 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { mdiShieldCheckOutline } from '@mdi/js'
|
||||
import { useRouter } from 'next/router'
|
||||
import BaseIcon from './BaseIcon'
|
||||
import { aiResponse } from '../stores/openAiSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
|
||||
type CopilotRole = 'assistant' | 'user'
|
||||
|
||||
type CopilotMessage = {
|
||||
id: string
|
||||
role: CopilotRole
|
||||
content: string
|
||||
time?: string
|
||||
}
|
||||
|
||||
type PageContext = {
|
||||
title: string
|
||||
focus: string
|
||||
}
|
||||
|
||||
const pageContexts: Array<{ match: string; title: string; focus: string }> = [
|
||||
{
|
||||
match: '/governance-workbench',
|
||||
title: 'Governance workbench',
|
||||
focus: 'AI use-case intake, review paths, approval readiness, vendor assessments, and policy coverage.',
|
||||
},
|
||||
{
|
||||
match: '/dashboard',
|
||||
title: 'System overview',
|
||||
focus: 'Executive visibility across AI adoption, risk posture, approvals, audit activity, and ROI.',
|
||||
},
|
||||
{
|
||||
match: '/ai_use_cases',
|
||||
title: 'AI request register',
|
||||
focus: 'Registration of proposed AI workflows before they touch client work.',
|
||||
},
|
||||
{
|
||||
match: '/approval_steps',
|
||||
title: 'Approval queue',
|
||||
focus: 'Partner, general counsel, IT/security, and ethics review decisions.',
|
||||
},
|
||||
{
|
||||
match: '/usage_audit_logs',
|
||||
title: 'Usage and audit log',
|
||||
focus: 'Traceability for prompts, outputs, reviewers, decisions, and evidence.',
|
||||
},
|
||||
{
|
||||
match: '/review_exceptions',
|
||||
title: 'Review exceptions',
|
||||
focus: 'Human-review gaps, risky shortcuts, and items needing remediation.',
|
||||
},
|
||||
{
|
||||
match: '/ai_tools',
|
||||
title: 'AI tool registry',
|
||||
focus: 'Approved, suspended, and in-review AI tools with model and usage constraints.',
|
||||
},
|
||||
{
|
||||
match: '/vendors',
|
||||
title: 'Vendor registry',
|
||||
focus: 'AI vendors, commercial owners, contracts, due diligence, and status.',
|
||||
},
|
||||
{
|
||||
match: '/vendor_risk_assessments',
|
||||
title: 'Vendor risk assessment',
|
||||
focus: 'Security, data retention, client data use, subprocessors, deletion, and legal-specific risk.',
|
||||
},
|
||||
{
|
||||
match: '/integrations',
|
||||
title: 'Integrations',
|
||||
focus: 'Connections to DMS, practice management, identity, workflow, reporting, and AI model layers.',
|
||||
},
|
||||
{
|
||||
match: '/data_classifications',
|
||||
title: 'Data sensitivity',
|
||||
focus: 'Public, internal, confidential, privileged, and regulated matter data classifications.',
|
||||
},
|
||||
{
|
||||
match: '/policies',
|
||||
title: 'Policies and guardrails',
|
||||
focus: 'AI usage policy, client notices, review rules, consent language, and prohibited use cases.',
|
||||
},
|
||||
{
|
||||
match: '/human_review_checklists',
|
||||
title: 'Human review checklists',
|
||||
focus: 'Reviewer checklists before AI-assisted work is filed, sent, billed, or relied upon.',
|
||||
},
|
||||
{
|
||||
match: '/checklist_items',
|
||||
title: 'Checklist items',
|
||||
focus: 'Atomic controls for human review, privilege checks, source verification, and output validation.',
|
||||
},
|
||||
{
|
||||
match: '/training_courses',
|
||||
title: 'Training courses',
|
||||
focus: 'AI governance training for attorneys, legal ops, IT/security, and reviewers.',
|
||||
},
|
||||
{
|
||||
match: '/practice_groups',
|
||||
title: 'Practice groups',
|
||||
focus: 'Practice-specific AI adoption patterns, owners, risk profiles, and rollout readiness.',
|
||||
},
|
||||
{
|
||||
match: '/matter_types',
|
||||
title: 'Matter types',
|
||||
focus: 'Matter-specific governance for litigation, transactions, employment, privacy, and other work.',
|
||||
},
|
||||
{
|
||||
match: '/roles_catalog',
|
||||
title: 'Roles catalog',
|
||||
focus: 'Governance roles, approval authority, access boundaries, and accountability.',
|
||||
},
|
||||
]
|
||||
|
||||
const starterPrompts = [
|
||||
'Classify a deposition summary workflow and suggest the approval path.',
|
||||
'Draft vendor risk questions for a legal AI tool.',
|
||||
'What should we track to prove ROI for this AI workflow?',
|
||||
]
|
||||
|
||||
const welcomeMessage: CopilotMessage = {
|
||||
id: 'welcome',
|
||||
role: 'assistant',
|
||||
content:
|
||||
'I can help map AI use cases, vendor reviews, human-review controls, and ROI evidence for this Legal AI Governance Hub. Tell me the workflow, tool, or risk you want to reason through.',
|
||||
}
|
||||
|
||||
const getNow = () => {
|
||||
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const findPageContext = (path: string): PageContext => {
|
||||
const match = pageContexts.find((item) => path.startsWith(item.match))
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
title: match.title,
|
||||
focus: match.focus,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Legal AI Governance Hub',
|
||||
focus: 'Controlled AI adoption across legal use cases, tools, vendors, policies, approvals, and audit evidence.',
|
||||
}
|
||||
}
|
||||
|
||||
const extractTextFromResponse = (response: any): string => {
|
||||
const payload = response?.data || response
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
|
||||
if (typeof payload?.output_text === 'string') {
|
||||
return payload.output_text
|
||||
}
|
||||
|
||||
if (typeof payload?.text === 'string') {
|
||||
return payload.text
|
||||
}
|
||||
|
||||
if (typeof payload?.message === 'string') {
|
||||
return payload.message
|
||||
}
|
||||
|
||||
if (payload?.choices?.[0]?.message?.content) {
|
||||
return payload.choices[0].message.content
|
||||
}
|
||||
|
||||
if (Array.isArray(payload?.output)) {
|
||||
const outputText = payload.output
|
||||
.flatMap((item) => item?.content || [])
|
||||
.map((part) => part?.text || part?.content || '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
if (outputText.trim()) {
|
||||
return outputText
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: any) => {
|
||||
if (typeof error === 'string') {
|
||||
return error
|
||||
}
|
||||
|
||||
if (typeof error?.message === 'string') {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (typeof error?.error?.message === 'string') {
|
||||
return error.error.message
|
||||
}
|
||||
|
||||
if (typeof error?.data?.error?.message === 'string') {
|
||||
return error.data.error.message
|
||||
}
|
||||
|
||||
return 'The AI service did not return a usable response. Please try again.'
|
||||
}
|
||||
|
||||
const buildSystemPrompt = (page: PageContext, route: string, userLabel: string) => {
|
||||
return `You are the AI Governance Copilot inside Legal AI Governance Hub.
|
||||
|
||||
Product context:
|
||||
Legal AI Governance Hub is a control plane for law firms and legal departments adopting AI. It is not an AI lawyer and it is not a contract generation chatbot. It helps teams register AI use cases, classify data sensitivity, evaluate AI tools and vendors, route approvals, enforce human review, keep audit trails, and prove ROI.
|
||||
|
||||
Current app page: ${page.title}
|
||||
Current route: ${route}
|
||||
Current page focus: ${page.focus}
|
||||
Signed-in workspace user: ${userLabel}
|
||||
|
||||
Core modules available in the app:
|
||||
- AI use-case intake and risk classification
|
||||
- Matter and client data sensitivity: public, internal, confidential, privileged, regulated
|
||||
- AI tool registry: ChatGPT, Claude, Gemini, Harvey, CoCounsel, Lexis, Spellbook, internal tools
|
||||
- Vendor registry and vendor risk assessments
|
||||
- Approval workflows: partner, general counsel, IT/security, ethics/risk
|
||||
- Policy and guardrail library
|
||||
- Human-review checklists
|
||||
- Usage and audit logs
|
||||
- Training tracker
|
||||
- ROI dashboard and adoption evidence
|
||||
- Integrations with DMS, practice management, identity, workflow, reporting, and model providers
|
||||
|
||||
Behavior rules:
|
||||
- Give practical governance and workflow guidance, not legal advice.
|
||||
- Do not claim that you changed application data. You can suggest what the user should add or review.
|
||||
- When useful, answer with concrete fields, approval steps, risks, and next actions.
|
||||
- Prefer concise, executive-ready language for legal operations and firm leadership.
|
||||
- Mention confidentiality, privilege, human review, vendor risk, and ROI when relevant.`
|
||||
}
|
||||
|
||||
export default function AiGovernanceCopilot() {
|
||||
const dispatch = useAppDispatch()
|
||||
const router = useRouter()
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
const isAskingResponse = useAppSelector((state) => state.openAi.isAskingResponse)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [input, setInput] = useState('')
|
||||
const [messages, setMessages] = useState<CopilotMessage[]>([welcomeMessage])
|
||||
const [localError, setLocalError] = useState('')
|
||||
const messagesEndRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const page = useMemo(() => findPageContext(router.pathname), [router.pathname])
|
||||
const userLabel = currentUser?.email || currentUser?.firstName || 'authenticated user'
|
||||
const panelSizeClass = isExpanded
|
||||
? 'fixed bottom-4 right-4 h-[calc(100vh-2rem)] w-[920px] max-w-[calc(100vw-2rem)] max-sm:bottom-3 max-sm:right-3 max-sm:h-[calc(100vh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)]'
|
||||
: 'fixed bottom-5 right-5 h-[720px] max-h-[calc(100vh-4rem)] w-[560px] max-w-[calc(100vw-2rem)] max-sm:bottom-3 max-sm:right-3 max-sm:h-[calc(100vh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)]'
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
|
||||
}, [messages, isOpen])
|
||||
|
||||
const askCopilot = async (prompt: string) => {
|
||||
const cleanPrompt = prompt.trim()
|
||||
|
||||
if (!cleanPrompt || isAskingResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
setLocalError('')
|
||||
setInput('')
|
||||
|
||||
const userMessage: CopilotMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: 'user',
|
||||
content: cleanPrompt,
|
||||
time: getNow(),
|
||||
}
|
||||
|
||||
const history = messages.filter((message) => message.id !== welcomeMessage.id)
|
||||
setMessages((currentMessages) => [...currentMessages, userMessage])
|
||||
|
||||
try {
|
||||
const response = await dispatch(
|
||||
aiResponse({
|
||||
input: [
|
||||
{ role: 'system', content: buildSystemPrompt(page, router.asPath, userLabel) },
|
||||
...history.map((message) => ({ role: message.role, content: message.content })),
|
||||
{ role: 'user', content: cleanPrompt },
|
||||
],
|
||||
options: {
|
||||
poll_interval: 3,
|
||||
poll_timeout: 180,
|
||||
},
|
||||
}),
|
||||
).unwrap()
|
||||
|
||||
const responseText = extractTextFromResponse(response).trim()
|
||||
|
||||
if (!responseText) {
|
||||
throw new Error('The AI service returned an empty response.')
|
||||
}
|
||||
|
||||
const assistantMessage: CopilotMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: responseText,
|
||||
time: getNow(),
|
||||
}
|
||||
|
||||
setMessages((currentMessages) => [...currentMessages, assistantMessage])
|
||||
} catch (error) {
|
||||
console.error('AI Governance Copilot failed', error)
|
||||
const errorMessage = getErrorMessage(error)
|
||||
setLocalError(errorMessage)
|
||||
setMessages((currentMessages) => [
|
||||
...currentMessages,
|
||||
{
|
||||
id: `assistant-error-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: `I could not complete that request. ${errorMessage}`,
|
||||
time: getNow(),
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
await askCopilot(input)
|
||||
}
|
||||
|
||||
const handleSuggestion = (prompt: string) => {
|
||||
setInput(prompt)
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-5 right-5 z-50 flex items-center gap-3 rounded-md border border-[#D8B75E]/50 bg-[#0E1A2B] px-4 py-3 text-left text-sm font-semibold text-white shadow-2xl shadow-[#0E1A2B]/25 transition hover:-translate-y-0.5 hover:border-[#D8B75E] hover:bg-[#13233A] focus:outline-none focus:ring-2 focus:ring-[#D8B75E] focus:ring-offset-2 focus:ring-offset-[#F6F3EC] dark:border-[#D8B75E]/60 dark:bg-[#D8B75E] dark:text-[#08111F] dark:shadow-black/40 dark:hover:bg-[#F0D98A] dark:focus:ring-offset-[#08111F]"
|
||||
aria-label="Open AI Governance Copilot"
|
||||
>
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-white/10 text-[#D8B75E] dark:bg-[#08111F] dark:text-[#D8B75E]">
|
||||
<BaseIcon path={mdiShieldCheckOutline} size={22} />
|
||||
</span>
|
||||
<span className="hidden leading-tight sm:block">
|
||||
<span className="block text-[11px] uppercase tracking-[0.18em] text-[#D8B75E] dark:text-[#7A5B13]">AI copilot</span>
|
||||
<span className="block">Ask governance</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`${panelSizeClass} z-50 flex flex-col overflow-hidden rounded-xl border border-[#D8CBB9] bg-[#FBFAF7] shadow-2xl shadow-[#0E1A2B]/25 transition-all duration-200 dark:border-[#273447] dark:bg-[#0B1424] dark:shadow-black/50`}
|
||||
aria-label="AI Governance Copilot"
|
||||
>
|
||||
<header className="border-b border-[#E5DDCF] bg-[#0E1A2B] px-4 py-4 text-white dark:border-[#273447]">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-[#D8B75E] text-[#08111F]">
|
||||
<BaseIcon path={mdiShieldCheckOutline} size={22} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#D8B75E]">Legal AI control plane</p>
|
||||
<h2 className="mt-1 text-lg font-semibold leading-tight">AI Governance Copilot</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-300">Context: {page.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded((currentValue) => !currentValue)}
|
||||
className="flex h-9 min-w-[4.25rem] items-center justify-center rounded-md border border-white/15 px-3 text-sm font-semibold text-slate-200 transition hover:border-[#D8B75E] hover:text-white focus:outline-none focus:ring-2 focus:ring-[#D8B75E]"
|
||||
aria-label={isExpanded ? 'Reduce AI Governance Copilot' : 'Expand AI Governance Copilot'}
|
||||
>
|
||||
{isExpanded ? 'Min' : 'Max'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md border border-white/15 text-lg font-semibold text-slate-200 transition hover:border-[#D8B75E] hover:text-white focus:outline-none focus:ring-2 focus:ring-[#D8B75E]"
|
||||
aria-label="Close AI Governance Copilot"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="border-b border-[#E5DDCF] bg-[#F6F3EC] px-4 py-2.5 dark:border-[#273447] dark:bg-[#111C2E]">
|
||||
<div className="rounded-lg border border-[#E2D7C4] bg-white px-3 py-2.5 dark:border-[#273447] dark:bg-[#0B1424]">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[#7A5B13] dark:text-[#D8B75E]">Workspace context</p>
|
||||
<p className="mt-1 text-sm leading-5 text-[#374151] dark:text-slate-300">{page.focus}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<article
|
||||
key={message.id}
|
||||
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[88%] rounded-xl px-4 py-3 text-sm leading-6 shadow-sm ${
|
||||
message.role === 'user'
|
||||
? 'bg-[#0E1A2B] text-white dark:bg-[#D8B75E] dark:text-[#08111F]'
|
||||
: 'border border-[#E2D7C4] bg-white text-[#1F2937] dark:border-[#273447] dark:bg-[#111C2E] dark:text-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-line">{message.content}</div>
|
||||
{message.time ? (
|
||||
<div
|
||||
className={`mt-2 text-[11px] ${
|
||||
message.role === 'user' ? 'text-slate-300 dark:text-[#7A5B13]' : 'text-[#8A8172] dark:text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{message.time}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
{isAskingResponse ? (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-xl border border-[#E2D7C4] bg-white px-4 py-3 text-sm text-[#4B5563] shadow-sm dark:border-[#273447] dark:bg-[#111C2E] dark:text-slate-300">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[#D8B75E]" />
|
||||
Reviewing governance context...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[#E5DDCF] bg-white px-4 py-4 dark:border-[#273447] dark:bg-[#0B1424]">
|
||||
<div className="mb-3 flex gap-2 overflow-x-auto pb-1">
|
||||
{starterPrompts.map((prompt) => (
|
||||
<button
|
||||
key={prompt}
|
||||
type="button"
|
||||
onClick={() => handleSuggestion(prompt)}
|
||||
className="shrink-0 rounded-md border border-[#D8CBB9] bg-[#F6F3EC] px-3 py-2 text-xs font-semibold text-[#374151] transition hover:border-[#D8B75E] hover:text-[#0E1A2B] focus:outline-none focus:ring-2 focus:ring-[#D8B75E] dark:border-[#273447] dark:bg-[#111C2E] dark:text-slate-300 dark:hover:border-[#D8B75E] dark:hover:text-white"
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{localError ? (
|
||||
<p className="mb-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs leading-5 text-red-700 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-200">
|
||||
{localError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
void askCopilot(input)
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
placeholder="Ask about an AI use case, vendor risk, review path, policy gap, or ROI evidence..."
|
||||
className="w-full resize-none rounded-lg border border-[#D8CBB9] bg-[#FBFAF7] px-3 py-3 text-sm leading-6 text-[#0E1A2B] placeholder:text-[#8A8172] focus:border-[#D8B75E] focus:outline-none focus:ring-2 focus:ring-[#D8B75E]/40 dark:border-[#273447] dark:bg-[#08111F] dark:text-slate-100 dark:placeholder:text-slate-500"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs leading-5 text-[#6B7280] dark:text-slate-500">Governance support only. Not legal advice.</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!input.trim() || isAskingResponse}
|
||||
className="rounded-md bg-[#0E1A2B] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-[#13233A] focus:outline-none focus:ring-2 focus:ring-[#D8B75E] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#D8B75E] dark:text-[#08111F] dark:hover:bg-[#F0D98A]"
|
||||
>
|
||||
{isAskingResponse ? 'Thinking' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -8,6 +8,7 @@ import NavBar from '../components/NavBar'
|
||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
||||
import AsideMenu from '../components/AsideMenu'
|
||||
import FooterBar from '../components/FooterBar'
|
||||
import AiGovernanceCopilot from '../components/AiGovernanceCopilot'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
import Search from '../components/Search';
|
||||
import { useRouter } from 'next/router'
|
||||
@ -156,6 +157,7 @@ export default function LayoutAuthenticated({
|
||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||
/>
|
||||
{children}
|
||||
<AiGovernanceCopilot />
|
||||
<FooterBar>Controlled AI adoption for legal teams.</FooterBar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user