Skip to content

Commit d1f68a4

Browse files
fix[frontend](home): make agent responses be on the chat hero home page
1 parent 47b602e commit d1f68a4

6 files changed

Lines changed: 205 additions & 152 deletions

File tree

frontend/src/features/home/components/ChatHero.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function ChatHero() {
2020
const send = () => {
2121
const text = value.trim()
2222
if (!text) return
23-
submit(text)
23+
submit(text, { openPanel: false, scope: 'home' })
2424
setValue('')
2525
}
2626

@@ -68,7 +68,7 @@ export function ChatHero() {
6868
return (
6969
<button
7070
key={key}
71-
onClick={() => submit(label)}
71+
onClick={() => submit(label, { openPanel: false, scope: 'home' })}
7272
className="rounded-full border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
7373
>
7474
{label}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useTranslation } from 'react-i18next'
2+
import { Trash2 } from 'lucide-react'
3+
import { useSocAi } from '@/features/soc-ai/SocAiProvider'
4+
import { MessageRow } from '@/features/soc-ai/components/MessageRow'
5+
6+
export function HomeChatTranscript() {
7+
const { t } = useTranslation()
8+
const { homeMessages, clear } = useSocAi()
9+
10+
return (
11+
<section className="mt-8">
12+
<div className="flex justify-end">
13+
<button
14+
type="button"
15+
onClick={() => clear('home')}
16+
className="inline-flex items-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:border-border hover:bg-muted hover:text-foreground"
17+
>
18+
<Trash2 size={13} />
19+
{t('socAi.chat.clear')}
20+
</button>
21+
</div>
22+
<div className="mt-4 flex flex-col gap-4 text-[13.5px] leading-relaxed">
23+
{homeMessages.map((m) => (
24+
<MessageRow key={m.id} message={m} />
25+
))}
26+
</div>
27+
</section>
28+
)
29+
}

frontend/src/features/home/pages/HomePage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ import { SystemHealthCard } from '../components/SystemHealthCard'
99
import { MitreTechniquesCard } from '../components/MitreTechniquesCard'
1010
import { TopAssetsCard } from '../components/TopAssetsCard'
1111
import { ComplianceCard } from '../components/ComplianceCard'
12+
import { useSocAi } from '@/features/soc-ai/SocAiProvider'
13+
import { HomeChatTranscript } from '../components/HomeChatTranscript'
1214

1315
export function HomePage() {
1416
const { t } = useTranslation()
1517
const alerts = useAlertKpis()
1618
const incidents = useOpenIncidents()
1719
const playbooks = useActivePlaybooks()
20+
const { homeMessages } = useSocAi()
1821

1922
return (
2023
<div className="mx-auto w-full max-w-[1100px] px-6 py-10">
2124
<ChatHero />
2225

23-
<div className="mt-10 grid grid-cols-12 gap-4">
26+
{homeMessages.length === 0 ? (
27+
<div className="mt-10 grid grid-cols-12 gap-4">
2428
<div className="col-span-6 md:col-span-3">
2529
<KpiTile
2630
icon={AlertTriangle}
@@ -82,7 +86,10 @@ export function HomePage() {
8286
<div className="col-span-12">
8387
<ComplianceCard />
8488
</div>
85-
</div>
89+
</div>
90+
) : (
91+
<HomeChatTranscript />
92+
)}
8693
</div>
8794
)
8895
}

frontend/src/features/soc-ai/SocAiProvider.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'
1+
import { createContext, useCallback, useContext, useMemo, useRef, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react'
22
import { useLocation } from 'react-router-dom'
33
import { useTranslation } from 'react-i18next'
44
import { extractNavigation, streamChat, type NavAction } from './lib/chat-stream'
@@ -18,16 +18,19 @@ export interface SocAiMessage {
1818
actions?: NavAction[]
1919
}
2020

21+
export type SocAiScope = 'panel' | 'home'
22+
2123
interface SocAiContextValue {
2224
open: boolean
2325
expanded: boolean
2426
messages: SocAiMessage[]
27+
homeMessages: SocAiMessage[]
2528
openPanel: () => void
2629
closePanel: () => void
2730
togglePanel: () => void
2831
toggleExpand: () => void
29-
submit: (text: string) => void
30-
clear: () => void
32+
submit: (text: string, opts?: { openPanel?: boolean; scope?: SocAiScope }) => void
33+
clear: (scope?: SocAiScope) => void
3134
}
3235

3336
const SocAiContext = createContext<SocAiContextValue | null>(null)
@@ -56,36 +59,43 @@ export function SocAiProvider({ children }: { children: ReactNode }) {
5659
const [open, setOpen] = useState(false)
5760
const [expanded, setExpanded] = useState(false)
5861
const [messages, setMessages] = useState<SocAiMessage[]>([])
62+
const [homeMessages, setHomeMessages] = useState<SocAiMessage[]>([])
5963
const idRef = useRef(0)
6064
const nextId = () => ++idRef.current
6165
const abortRef = useRef<AbortController | null>(null)
6266
const location = useLocation()
6367
const { t, i18n } = useTranslation()
6468

69+
const setters: Record<SocAiScope, Dispatch<SetStateAction<SocAiMessage[]>>> = {
70+
panel: setMessages,
71+
home: setHomeMessages,
72+
}
73+
6574
const openPanel = useCallback(() => setOpen(true), [])
6675
const closePanel = useCallback(() => setOpen(false), [])
6776
const togglePanel = useCallback(() => setOpen((v) => !v), [])
6877
const toggleExpand = useCallback(() => setExpanded((v) => !v), [])
69-
const clear = useCallback(() => {
78+
const clear = useCallback((scope: SocAiScope = 'panel') => {
7079
abortRef.current?.abort()
71-
setMessages([])
80+
setters[scope]([])
7281
}, [])
7382

74-
const patchMsg = useCallback((id: number, fn: (m: SocAiMessage) => SocAiMessage) => {
75-
setMessages((list) => list.map((m) => (m.id === id ? fn(m) : m)))
83+
const patchMsg = useCallback((scope: SocAiScope, id: number, fn: (m: SocAiMessage) => SocAiMessage) => {
84+
setters[scope]((list) => list.map((m) => (m.id === id ? fn(m) : m)))
7685
}, [])
7786

7887
const submit = useCallback(
79-
(raw: string) => {
88+
(raw: string, opts?: { openPanel?: boolean; scope?: SocAiScope }) => {
8089
const text = raw.trim()
8190
if (!text) return
82-
setOpen(true)
91+
const scope: SocAiScope = opts?.scope ?? 'panel'
92+
if (opts?.openPanel !== false) setOpen(true)
8393
abortRef.current?.abort()
8494
const ac = new AbortController()
8595
abortRef.current = ac
8696

8797
const aiId = nextId()
88-
setMessages((m) => [
98+
setters[scope]((m) => [
8999
...m,
90100
{ id: nextId(), role: 'user', text },
91101
{ id: aiId, role: 'ai', text: '', pending: true, steps: [] },
@@ -97,7 +107,7 @@ export function SocAiProvider({ children }: { children: ReactNode }) {
97107
streamChat(
98108
{ task: text, page, lang },
99109
(ev) => {
100-
patchMsg(aiId, (msg) => {
110+
patchMsg(scope, aiId, (msg) => {
101111
switch (ev.kind) {
102112
case 'tool_call':
103113
return { ...msg, steps: [...(msg.steps ?? []), { tool: ev.tool ?? 'tool', status: 'running' }] }
@@ -125,7 +135,7 @@ export function SocAiProvider({ children }: { children: ReactNode }) {
125135
ac.signal,
126136
).catch((err) => {
127137
if (ac.signal.aborted) return
128-
patchMsg(aiId, (msg) => ({
138+
patchMsg(scope, aiId, (msg) => ({
129139
...msg,
130140
text: err instanceof Error ? err.message : t('socAi.chat.errorUnreachable'),
131141
error: true,
@@ -137,8 +147,8 @@ export function SocAiProvider({ children }: { children: ReactNode }) {
137147
)
138148

139149
const value = useMemo(
140-
() => ({ open, expanded, messages, openPanel, closePanel, togglePanel, toggleExpand, submit, clear }),
141-
[open, expanded, messages, openPanel, closePanel, togglePanel, toggleExpand, submit, clear],
150+
() => ({ open, expanded, messages, homeMessages, openPanel, closePanel, togglePanel, toggleExpand, submit, clear }),
151+
[open, expanded, messages, homeMessages, openPanel, closePanel, togglePanel, toggleExpand, submit, clear],
142152
)
143153

144154
return <SocAiContext.Provider value={value}>{children}</SocAiContext.Provider>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { useNavigate } from 'react-router-dom'
4+
import { AlertCircle, ArrowUpRight, Check, Copy, Loader2, Wrench, X } from 'lucide-react'
5+
import { cn } from '@/shared/lib/utils'
6+
import { useSocAi, type SocAiMessage, type ToolStep } from '../SocAiProvider'
7+
import type { NavAction } from '../lib/chat-stream'
8+
import { MarkdownMessage } from './MarkdownMessage'
9+
10+
// Logical navigation destinations the agent may emit → app routes. Filters/time
11+
// travel in router state for the target page to apply (where supported).
12+
const DEST_ROUTE: Record<string, string> = {
13+
'log-explorer': '/log-explorer',
14+
alerts: '/threat-management/alerts',
15+
incidents: '/threat-management/incidents',
16+
adversaries: '/threat-management/adversaries',
17+
compliance: '/compliance',
18+
datasources: '/datasources',
19+
dashboards: '/home',
20+
}
21+
22+
export function MessageRow({ message }: { message: SocAiMessage }) {
23+
if (message.role === 'user') {
24+
return (
25+
<div className="max-w-[80%] self-end rounded-2xl rounded-br-md bg-foreground/[0.07] px-3.5 py-1.5 text-foreground [overflow-wrap:anywhere]">
26+
{message.text}
27+
</div>
28+
)
29+
}
30+
31+
const hasSteps = (message.steps?.length ?? 0) > 0
32+
return (
33+
<div className="self-stretch">
34+
{hasSteps && <Steps steps={message.steps!} />}
35+
{message.pending && !message.text ? (
36+
<TypingDots />
37+
) : message.error ? (
38+
<div className="flex items-start gap-2 rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-600 dark:text-red-300">
39+
<AlertCircle size={14} className="mt-0.5 shrink-0" />
40+
<span>{message.text}</span>
41+
</div>
42+
) : (
43+
<>
44+
<MarkdownMessage text={message.text} />
45+
{message.actions && message.actions.length > 0 && <Actions actions={message.actions} />}
46+
<CopyButton text={message.text} />
47+
</>
48+
)}
49+
</div>
50+
)
51+
}
52+
53+
function Steps({ steps }: { steps: ToolStep[] }) {
54+
return (
55+
<div className="mb-2 space-y-1 rounded-md border border-border bg-muted/30 px-2.5 py-2">
56+
{steps.map((s, i) => (
57+
<div key={i} className="flex items-center gap-2 text-[11px] text-muted-foreground">
58+
{s.status === 'running' ? (
59+
<Loader2 size={12} className="shrink-0 animate-spin" />
60+
) : s.status === 'error' ? (
61+
<X size={12} className="shrink-0 text-red-500" />
62+
) : (
63+
<Check size={12} className="shrink-0 text-emerald-500" />
64+
)}
65+
<Wrench size={11} className="shrink-0 opacity-60" />
66+
<span className="truncate font-mono">{s.tool}</span>
67+
</div>
68+
))}
69+
</div>
70+
)
71+
}
72+
73+
function Actions({ actions }: { actions: NavAction[] }) {
74+
const navigate = useNavigate()
75+
const { closePanel } = useSocAi()
76+
77+
const go = (a: NavAction) => {
78+
const route = DEST_ROUTE[a.destination]
79+
if (!route) return
80+
closePanel()
81+
navigate(route, { state: { socaiFilters: a.filters ?? [], socaiTime: a.time } })
82+
}
83+
84+
return (
85+
<div className="mt-3 flex flex-wrap gap-2">
86+
{actions
87+
.filter((a) => DEST_ROUTE[a.destination])
88+
.map((a, i) => (
89+
<button
90+
key={i}
91+
type="button"
92+
onClick={() => go(a)}
93+
className="inline-flex items-center gap-1.5 rounded-md border border-primary/40 bg-primary/5 px-2.5 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-primary/10"
94+
>
95+
{a.label || 'Open'}
96+
<ArrowUpRight size={13} />
97+
</button>
98+
))}
99+
</div>
100+
)
101+
}
102+
103+
function TypingDots() {
104+
return (
105+
<div className="flex items-center gap-1 py-1 text-muted-foreground">
106+
{[0, 1, 2].map((i) => (
107+
<span
108+
key={i}
109+
className="h-1.5 w-1.5 animate-bounce rounded-full bg-current"
110+
style={{ animationDelay: `${i * 120}ms` }}
111+
/>
112+
))}
113+
</div>
114+
)
115+
}
116+
117+
function CopyButton({ text }: { text: string }) {
118+
const { t } = useTranslation()
119+
const [copied, setCopied] = useState(false)
120+
const copy = () => {
121+
void navigator.clipboard?.writeText(text)
122+
setCopied(true)
123+
window.setTimeout(() => setCopied(false), 1500)
124+
}
125+
return (
126+
<button
127+
type="button"
128+
onClick={copy}
129+
className={cn(
130+
'mt-2.5 inline-flex h-6 items-center gap-1.5 rounded-md border border-transparent px-2 text-xs font-medium text-muted-foreground transition-colors hover:border-border hover:bg-muted hover:text-foreground',
131+
copied && 'border-primary/35 bg-primary/10 text-primary',
132+
)}
133+
>
134+
{copied ? <Check size={13} /> : <Copy size={13} />}
135+
{copied ? t('socAi.chat.copied') : t('socAi.chat.copy')}
136+
</button>
137+
)
138+
}

0 commit comments

Comments
 (0)