Add AI governance copilot
This commit is contained in:
parent
7aefb06035
commit
5ff41e2af4
479
frontend/src/components/AiGovernanceCopilot.tsx
Normal file
479
frontend/src/components/AiGovernanceCopilot.tsx
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
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 [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'
|
||||||
|
|
||||||
|
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="fixed bottom-5 right-5 z-50 flex h-[620px] max-h-[calc(100vh-5rem)] w-[420px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-xl border border-[#D8CBB9] bg-[#FBFAF7] shadow-2xl shadow-[#0E1A2B]/25 dark:border-[#273447] dark:bg-[#0B1424] dark:shadow-black/50 max-sm:bottom-3 max-sm:right-3 max-sm:h-[calc(100vh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)]"
|
||||||
|
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>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="flex h-9 w-9 shrink-0 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>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="border-b border-[#E5DDCF] bg-[#F6F3EC] px-4 py-3 dark:border-[#273447] dark:bg-[#111C2E]">
|
||||||
|
<div className="rounded-lg border border-[#E2D7C4] bg-white p-3 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user