Skip to content

Commit 44e3484

Browse files
authored
add session type selector for non agent chat threads (#2538)
1 parent 3b9f0bc commit 44e3484

File tree

11 files changed

+211
-58
lines changed

11 files changed

+211
-58
lines changed

assets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@nivo/radial-bar": "0.99.0",
4848
"@nivo/tooltip": "0.99.0",
4949
"@nivo/treemap": "0.99.0",
50-
"@pluralsh/design-system": "5.23.1",
50+
"@pluralsh/design-system": "5.23.2",
5151
"@react-hooks-library/core": "0.6.0",
5252
"@react-spring/web": "10.0.1",
5353
"@saas-ui/use-hotkeys": "1.1.3",

assets/src/components/ai/AIAgent.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '../../generated/graphql.ts'
1717
import { isEmpty } from 'lodash'
1818
import { EmptyStateCompact } from './AIThreads.tsx'
19-
import { useMemo } from 'react'
19+
import { useMemo, useRef } from 'react'
2020
import { mapExistingNodes } from '../../utils/graphql.ts'
2121
import { GqlError } from '../utils/Alert.tsx'
2222
import { TableSkeleton } from '../utils/SkeletonLoaders.tsx'
@@ -30,6 +30,9 @@ import {
3030
isAfter,
3131
} from '../../utils/datetime.ts'
3232
import { useChatbot } from './AIContext.tsx'
33+
import { useNativeDomEvent } from 'components/hooks/useNativeDomEvent.tsx'
34+
35+
export const CLOSE_CHAT_ACTION_PANEL_EVENT = 'pointerdown'
3336

3437
export function AIAgent() {
3538
const theme = useTheme()
@@ -119,6 +122,15 @@ const columns = [
119122
const theme = useTheme()
120123
const { goToThread } = useChatbot()
121124

125+
// need to do this natively so it stops propagation correctly, see Console.tsx
126+
const wrapperRef = useRef<HTMLDivElement>(null)
127+
useNativeDomEvent(wrapperRef, CLOSE_CHAT_ACTION_PANEL_EVENT, (e) => {
128+
if (agentSession?.thread) {
129+
e.stopPropagation()
130+
goToThread(agentSession.thread.id)
131+
}
132+
})
133+
122134
const agentSession = getValue()
123135

124136
const timestamp =
@@ -130,6 +142,7 @@ const columns = [
130142

131143
return (
132144
<div
145+
ref={wrapperRef}
133146
css={{
134147
display: 'flex',
135148
alignItems: 'center',
@@ -144,13 +157,6 @@ const columns = [
144157
},
145158
},
146159
}}
147-
// this needs to be pointerDown instead of onclick to prevent closing the action panel (they need to use the same event type)
148-
onPointerDown={(e) => {
149-
if (agentSession?.thread) {
150-
e.stopPropagation()
151-
goToThread(agentSession.thread.id)
152-
}
153-
}}
154160
>
155161
<Flex
156162
alignItems="center"

assets/src/components/ai/AIContext.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type ChatbotContextT = {
4949
open: boolean
5050
setOpen: (open: boolean) => void
5151

52+
actionsPanelOpen: boolean
53+
setActionsPanelOpen: (show: boolean) => void
54+
5255
setShowForkToast: (show: boolean) => void
5356

5457
currentThread: Nullable<ChatThreadFragment>
@@ -89,6 +92,7 @@ export function AIContextProvider({ children }: { children: ReactNode }) {
8992
function ChatbotContextProvider({ children }: { children: ReactNode }) {
9093
const { spacing } = useTheme()
9194
const [open, setOpen] = usePersistedState('plural-ai-copilot-open', true)
95+
const [actionsPanelOpen, setActionsPanelOpen] = useState<boolean>(false)
9296
const [currentThreadId, setCurrentThreadId] = useState<Nullable<string>>()
9397
const [persistedThreadId, setPersistedThreadId] = usePersistedState<
9498
Nullable<string>
@@ -153,6 +157,8 @@ function ChatbotContextProvider({ children }: { children: ReactNode }) {
153157
persistedThreadId,
154158
lastNonAgentThreadId,
155159
setShowForkToast,
160+
actionsPanelOpen,
161+
setActionsPanelOpen,
156162
}}
157163
>
158164
{children}
@@ -205,6 +211,8 @@ export function useChatbot() {
205211
currentThread,
206212
currentThreadId,
207213
setCurrentThreadId,
214+
actionsPanelOpen,
215+
setActionsPanelOpen,
208216
persistedThreadId,
209217
lastNonAgentThreadId,
210218
selectedAgent,
@@ -267,6 +275,8 @@ export function useChatbot() {
267275
goToThread(lastNonAgentThreadId)
268276
},
269277
closeChatbot: () => setOpen(false),
278+
actionsPanelOpen,
279+
setActionsPanelOpen,
270280
currentThread,
271281
currentThreadId,
272282
persistedThreadId,

assets/src/components/ai/chatbot/AgentSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function AgentSelect() {
124124
style={{
125125
pointerEvents: 'none',
126126
transition: 'transform 0.2s ease-in-out',
127-
transform: open ? 'scaleY(-1)' : 'scaleY(1)',
127+
transform: open ? 'scaleY(1)' : 'scaleY(-1)',
128128
}}
129129
/>
130130
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
Button,
3+
CaretUpIcon,
4+
KubernetesIcon,
5+
ListBoxItem,
6+
RocketIcon,
7+
SearchIcon,
8+
Select,
9+
Toast,
10+
Tooltip,
11+
} from '@pluralsh/design-system'
12+
import { useChatbot } from 'components/ai/AIContext'
13+
import { TRUNCATE } from 'components/utils/truncate.ts'
14+
import {
15+
AgentSessionType,
16+
useUpdateChatThreadMutation,
17+
} from 'generated/graphql'
18+
import { capitalize } from 'lodash'
19+
import { useCallback, useState } from 'react'
20+
21+
export function AgentSessionTypeSelect() {
22+
const { currentThread } = useChatbot()
23+
const [open, setOpen] = useState(false)
24+
25+
const [mutation, { loading: loading, error }] = useUpdateChatThreadMutation()
26+
const updateSessionType = useCallback(
27+
(type: AgentSessionType | null) => {
28+
if (!currentThread?.id || type === currentThread?.session?.type) return
29+
mutation({
30+
variables: {
31+
id: currentThread?.id,
32+
attributes: { session: { type }, summary: currentThread?.summary },
33+
},
34+
})
35+
},
36+
[currentThread, mutation]
37+
)
38+
39+
const curType = currentThread?.session?.type
40+
41+
if (
42+
!curType ||
43+
curType === AgentSessionType.Kubernetes ||
44+
curType === AgentSessionType.Terraform
45+
)
46+
return null
47+
48+
return (
49+
<>
50+
<Select
51+
isOpen={open}
52+
onOpenChange={setOpen}
53+
selectedKey={curType ?? ''}
54+
onSelectionChange={(key) =>
55+
updateSessionType(key ? (key as AgentSessionType) : null)
56+
}
57+
width={260}
58+
triggerButton={
59+
<Button
60+
small
61+
loading={loading}
62+
secondary
63+
startIcon={options.find((o) => o.type === curType)?.icon}
64+
endIcon={
65+
<CaretUpIcon
66+
style={{
67+
pointerEvents: 'none',
68+
transition: 'transform 0.2s ease-in-out',
69+
transform: open ? 'scaleY(1)' : 'scaleY(-1)',
70+
}}
71+
/>
72+
}
73+
>
74+
<Tooltip
75+
css={{ maxWidth: 500 }}
76+
placement="top"
77+
label="Change the type of session you are having with Plural Copilot"
78+
>
79+
<span css={{ ...TRUNCATE }}>
80+
{capitalize(curType ?? 'Change session type')}
81+
</span>
82+
</Tooltip>
83+
</Button>
84+
}
85+
>
86+
{options.map(({ type, description, icon }) => (
87+
<ListBoxItem
88+
key={type}
89+
label={capitalize(type)}
90+
description={description}
91+
leftContent={icon}
92+
/>
93+
))}
94+
</Select>
95+
<Toast
96+
show={!!error}
97+
closeTimeout={5000}
98+
severity="danger"
99+
position="bottom"
100+
marginBottom="medium"
101+
>
102+
<strong>Error updating session type:</strong> {error?.message}
103+
</Toast>
104+
</>
105+
)
106+
}
107+
108+
const options = [
109+
{
110+
type: AgentSessionType.Manifests,
111+
description: 'Author K8s yaml',
112+
icon: <KubernetesIcon />,
113+
},
114+
{
115+
type: AgentSessionType.Provisioning,
116+
description: 'Provision new infra',
117+
icon: <RocketIcon />,
118+
},
119+
{
120+
type: AgentSessionType.Search,
121+
description: 'Query your existing infra',
122+
icon: <SearchIcon />,
123+
},
124+
]

assets/src/components/ai/chatbot/Chatbot.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
} from './ChatbotPanelThread.tsx'
3030
import { McpServerShelf } from './tools/McpServerShelf.tsx'
3131
import { useResizablePane } from './useResizeableChatPane.tsx'
32-
import { useClickOutside } from '@react-hooks-library/core'
3332

3433
const MIN_WIDTH = 500
3534
const MAX_WIDTH_VW = 40
@@ -87,7 +86,6 @@ function ChatbotPanelInner() {
8786
mutationLoading,
8887
} = useChatbot()
8988
const [showMcpServers, setShowMcpServers] = useState(false)
90-
const [showActionsPanel, setShowActionsPanel] = useState<boolean>(false)
9189
const [showPrompts, setShowPrompts] = useState<boolean>(false)
9290

9391
const { data } = useFetchPaginatedData({
@@ -123,8 +121,6 @@ function ChatbotPanelInner() {
123121
const { calculatedPanelWidth, dragHandleProps, isDragging } =
124122
useResizablePane(MIN_WIDTH, MAX_WIDTH_VW)
125123

126-
useClickOutside(ref, () => setShowActionsPanel(false))
127-
128124
useEffect(() => {
129125
// If the agent is initializing, a thread doesn't need to be selected.
130126
if (agentInitMode) return
@@ -183,19 +179,13 @@ function ChatbotPanelInner() {
183179
)}
184180
{currentThread?.session && !agentInitMode && (
185181
<ChatbotActionsPanel
186-
isOpen={showActionsPanel}
187-
setOpen={setShowActionsPanel}
188182
zIndex={1}
189183
messages={messages}
190184
/>
191185
)}
192186
<MainContentWrapperSC>
193187
<ResizeGripSC />
194-
<ChatbotHeader
195-
currentThread={currentThread}
196-
isActionsPanelOpen={showActionsPanel}
197-
setIsActionsPanelOpen={setShowActionsPanel}
198-
/>
188+
<ChatbotHeader currentThread={currentThread} />
199189
{threadDetailsQuery.error?.error && (
200190
<GqlError error={threadDetailsQuery.error.error} />
201191
)}

assets/src/components/ai/chatbot/ChatbotHeader.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,18 @@ import {
2727
CommandPaletteTab,
2828
} from 'components/commandpalette/CommandPaletteContext.tsx'
2929
import { use } from 'react'
30+
import { AgentSessionTypeSelect } from './AgentSessionTypeSelect.tsx'
3031

3132
export function ChatbotHeader({
3233
currentThread,
33-
isActionsPanelOpen,
34-
setIsActionsPanelOpen,
3534
}: {
3635
currentThread?: Nullable<ChatThreadTinyFragment>
37-
isActionsPanelOpen: boolean
38-
setIsActionsPanelOpen: (isOpen: boolean) => void
3936
}) {
40-
const { colors, spacing } = useTheme()
37+
const { colors } = useTheme()
4138
const { setCmdkOpen, setInitialTab } = use(CommandPaletteContext)
4239
const {
40+
actionsPanelOpen,
41+
setActionsPanelOpen,
4342
agentInitMode,
4443
closeChatbot,
4544
createNewThread,
@@ -64,19 +63,17 @@ export function ChatbotHeader({
6463
<div
6564
css={{
6665
transition: 'transform 0.16s ease-in-out',
67-
transform: isActionsPanelOpen ? 'scaleX(-1)' : 'scaleX(1)',
66+
transform: actionsPanelOpen ? 'scaleX(-1)' : 'scaleX(1)',
6867
}}
6968
>
7069
<IconFrame
7170
clickable
7271
size="small"
7372
tooltip={
74-
isActionsPanelOpen
75-
? 'Close actions panel'
76-
: 'Open actions panel'
73+
actionsPanelOpen ? 'Close actions panel' : 'Open actions panel'
7774
}
7875
icon={<HamburgerMenuCollapseIcon />}
79-
onClick={() => setIsActionsPanelOpen(!isActionsPanelOpen)}
76+
onClick={() => setActionsPanelOpen(!actionsPanelOpen)}
8077
/>
8178
</div>
8279
)}
@@ -95,10 +92,7 @@ export function ChatbotHeader({
9592
createNewThread({
9693
summary: 'New chat with Plural Copilot',
9794
...(connectionId && {
98-
session: {
99-
connectionId,
100-
done: true,
101-
},
95+
session: { connectionId, done: true },
10296
}),
10397
})
10498
}
@@ -136,8 +130,8 @@ export function ChatbotHeader({
136130
firstPartialType="body2Bold"
137131
firstColor="text"
138132
secondPartialType="caption"
139-
css={{ flex: 1, paddingRight: spacing.large }}
140133
/>
134+
<AgentSessionTypeSelect />
141135
</SubHeaderSC>
142136
<Toast
143137
show={!!mutationError}
@@ -156,6 +150,8 @@ const SubHeaderSC = styled.div(({ theme }) => ({
156150
height: 48,
157151
display: 'flex',
158152
alignItems: 'center',
153+
gap: theme.spacing.large,
154+
justifyContent: 'space-between',
159155
padding: `0 ${theme.spacing.medium}px`,
160156
borderBottom: theme.borders.default,
161157
}))

0 commit comments

Comments
 (0)