Skip to content

Commit 8548a7b

Browse files
authored
feat(lungo): add vertically resizable agent chat panel (#715)
* refactor(lungo): extract shared ResizablePanelSeparator Signed-off-by: misi-bp <mkaloczy@cisco.com> * feat(lungo): add vertically resizable chat panel Signed-off-by: misi-bp <mkaloczy@cisco.com> * fix(lungo): single ChatArea mount and composer-only chat panel sizing Signed-off-by: misi-bp <mkaloczy@cisco.com> --------- Signed-off-by: misi-bp <mkaloczy@cisco.com>
1 parent e2c40c2 commit 8548a7b

11 files changed

Lines changed: 485 additions & 255 deletions

File tree

coffeeAGNTCY/coffee_agents/lungo/frontend/src/components/Chat/ChatArea.tsx

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { HttpRequestTarget } from "@/urls"
2222
import { CanvasMode } from "@/types/patternDoc"
2323
import { streamPatternChat } from "./streamPatternChat"
2424

25-
/** Panel expanded/collapsed by the chat header minimize control. */
25+
/** Scrollable message thread region between header and composer. */
2626
export const CHAT_MESSAGE_PANEL_ID = "chat-message-panel"
2727

2828
interface ChatAreaProps {
@@ -51,6 +51,8 @@ interface ChatAreaProps {
5151
canvasMode?: CanvasMode
5252
selectedReferencePattern?: string | null
5353
patternChatSessionId?: string | null
54+
/** When false, the chat shell sizes to its composer content only. */
55+
fillHeight?: boolean
5456
}
5557

5658
const ChatArea: React.FC<ChatAreaProps> = ({
@@ -79,40 +81,25 @@ const ChatArea: React.FC<ChatAreaProps> = ({
7981
canvasMode,
8082
selectedReferencePattern,
8183
patternChatSessionId,
84+
fillHeight = true,
8285
}) => {
8386
const [content, setContent] = useState<string>("")
8487
const [loading, setLoading] = useState<boolean>(false)
85-
const [isMinimized, setIsMinimized] = useState<boolean>(false)
8688
const messagePanelRef = useRef<HTMLDivElement>(null)
8789
const threadContentRef = useRef<HTMLDivElement>(null)
8890
const { sendPatternMessage } = usePatternChatAPI()
8991

9092
useScrollPanelOnContentResize(
9193
messagePanelRef,
9294
threadContentRef,
93-
Boolean(currentUserMessage) && !isMinimized,
95+
Boolean(currentUserMessage),
9496
)
9597

96-
const handleMinimize = () => {
97-
setIsMinimized(true)
98-
}
99-
100-
const handleRestore = () => {
101-
setIsMinimized(false)
102-
}
103-
10498
const handleSuggestedPromptSelect = (query: string) => {
105-
if (isMinimized) {
106-
setIsMinimized(false)
107-
}
10899
setContent(query)
109100
}
110101

111102
const processMessage = async (): Promise<void> => {
112-
if (isMinimized) {
113-
setIsMinimized(false)
114-
}
115-
116103
if (
117104
canvasMode === CanvasMode.PATTERN_DOC &&
118105
selectedReferencePattern &&
@@ -173,24 +160,22 @@ const ChatArea: React.FC<ChatAreaProps> = ({
173160
position: "relative",
174161
display: "flex",
175162
width: "100%",
176-
height: "100%",
177-
maxHeight: "100%",
163+
height: fillHeight ? "100%" : "auto",
164+
maxHeight: fillHeight ? "100%" : "none",
178165
minHeight: 0,
179166
flexDirection: "column",
180167
boxSizing: "border-box",
181168
overflow: "hidden",
182-
borderTop: "1px solid",
183-
borderColor: "divider",
184169
backgroundColor: (theme) => theme.palette.action.selected,
170+
...(!fillHeight
171+
? { borderTop: "1px solid", borderColor: "divider" }
172+
: {}),
185173
}}
186174
>
187175
{currentUserMessage ? (
188176
<Box sx={{ flexShrink: 0, width: "100%" }}>
189177
<ChatHeader
190-
onMinimize={isMinimized ? handleRestore : handleMinimize}
191178
onClearConversation={onClearConversation}
192-
isMinimized={isMinimized}
193-
messagePanelId={CHAT_MESSAGE_PANEL_ID}
194179
horizontalPadding={chatHorizontalPadding}
195180
/>
196181
</Box>
@@ -201,10 +186,9 @@ const ChatArea: React.FC<ChatAreaProps> = ({
201186
id={CHAT_MESSAGE_PANEL_ID}
202187
role="region"
203188
aria-label="Chat messages"
204-
aria-hidden={isMinimized ? true : undefined}
205189
sx={{
206-
flex: "1 1 auto",
207-
minHeight: 0,
190+
flex: fillHeight ? "1 1 auto" : "0 0 auto",
191+
minHeight: fillHeight ? 0 : undefined,
208192
width: "100%",
209193
overflowY: "auto",
210194
overflowX: "hidden",
@@ -225,13 +209,12 @@ const ChatArea: React.FC<ChatAreaProps> = ({
225209
width: "100%",
226210
px: chatHorizontalPadding,
227211
py: currentUserMessage ? 1 : 0,
228-
display: isMinimized ? "none" : "flex",
212+
display: "flex",
229213
}}
230214
>
231215
{currentUserMessage ? (
232216
<ChatAreaMessageThread
233217
currentUserMessage={currentUserMessage}
234-
isMinimized={isMinimized}
235218
showProgressTracker={showProgressTracker}
236219
showAuctionStreaming={showAuctionStreaming}
237220
showRecruiterStreaming={showRecruiterStreaming}

coffeeAGNTCY/coffee_agents/lungo/frontend/src/components/Chat/ChatAreaMessageThread.tsx

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import GrafanaSessionLink from "./GrafanaSessionLink"
2626

2727
export interface ChatAreaMessageThreadProps {
2828
currentUserMessage: string
29-
isMinimized: boolean
3029
showProgressTracker: boolean
3130
showAuctionStreaming: boolean
3231
showRecruiterStreaming: boolean
@@ -45,7 +44,6 @@ export interface ChatAreaMessageThreadProps {
4544

4645
const ChatAreaMessageThread: React.FC<ChatAreaMessageThreadProps> = ({
4746
currentUserMessage,
48-
isMinimized,
4947
showProgressTracker,
5048
showAuctionStreaming,
5149
showRecruiterStreaming,
@@ -65,8 +63,7 @@ const ChatAreaMessageThread: React.FC<ChatAreaMessageThreadProps> = ({
6563
const hasAgentFinalResponse =
6664
showFinalResponse &&
6765
!hasApiError &&
68-
(isAgentLoading || Boolean(agentResponse?.response?.trim())) &&
69-
!isMinimized
66+
(isAgentLoading || Boolean(agentResponse?.response?.trim()))
7067

7168
return (
7269
<Stack
@@ -91,53 +88,47 @@ const ChatAreaMessageThread: React.FC<ChatAreaMessageThreadProps> = ({
9188
</StatusMessage>
9289
) : null}
9390

94-
{!isMinimized && currentUserMessage.trim() ? (
91+
{currentUserMessage.trim() ? (
9592
<UserMessage content={currentUserMessage} />
9693
) : null}
9794

98-
{showProgressTracker && (
99-
<Box sx={{ width: "100%", display: isMinimized ? "none" : "block" }}>
100-
<GroupCommunicationFeed
101-
isVisible={!isMinimized && showProgressTracker}
102-
onComplete={onStreamComplete}
103-
onSenderHighlight={onSenderHighlight}
104-
graphConfig={graphConfig}
105-
prompt={currentUserMessage}
106-
executionKey={executionKey}
107-
apiError={hasApiError}
108-
observabilitySessionId={observabilitySessionId}
109-
/>
110-
</Box>
111-
)}
95+
{showProgressTracker ? (
96+
<GroupCommunicationFeed
97+
isVisible={showProgressTracker}
98+
onComplete={onStreamComplete}
99+
onSenderHighlight={onSenderHighlight}
100+
graphConfig={graphConfig}
101+
prompt={currentUserMessage}
102+
executionKey={executionKey}
103+
apiError={hasApiError}
104+
observabilitySessionId={observabilitySessionId}
105+
/>
106+
) : null}
112107

113-
{showAuctionStreaming && (
114-
<Box sx={{ width: "100%", display: isMinimized ? "none" : "block" }}>
115-
<AuctionStreamingFeed
116-
isVisible={!isMinimized && showAuctionStreaming}
117-
prompt={currentUserMessage}
118-
apiError={hasApiError}
119-
auctionStreamingState={auctionState}
120-
onSenderHighlight={onSenderHighlight}
121-
graphConfig={graphConfig}
122-
observabilitySessionId={observabilitySessionId}
123-
/>
124-
</Box>
125-
)}
108+
{showAuctionStreaming ? (
109+
<AuctionStreamingFeed
110+
isVisible={showAuctionStreaming}
111+
prompt={currentUserMessage}
112+
apiError={hasApiError}
113+
auctionStreamingState={auctionState}
114+
onSenderHighlight={onSenderHighlight}
115+
graphConfig={graphConfig}
116+
observabilitySessionId={observabilitySessionId}
117+
/>
118+
) : null}
126119

127-
{showRecruiterStreaming && (
128-
<Box sx={{ width: "100%", display: isMinimized ? "none" : "block" }}>
129-
<RecruiterStreamingFeed
130-
isVisible={!isMinimized && showRecruiterStreaming}
131-
prompt={currentUserMessage}
132-
apiError={hasApiError}
133-
recruiterStreamingState={recruiterState}
134-
onStreamComplete={onStreamComplete}
135-
onSenderHighlight={onSenderHighlight}
136-
graphConfig={graphConfig}
137-
observabilitySessionId={observabilitySessionId}
138-
/>
139-
</Box>
140-
)}
120+
{showRecruiterStreaming ? (
121+
<RecruiterStreamingFeed
122+
isVisible={showRecruiterStreaming}
123+
prompt={currentUserMessage}
124+
apiError={hasApiError}
125+
recruiterStreamingState={recruiterState}
126+
onStreamComplete={onStreamComplete}
127+
onSenderHighlight={onSenderHighlight}
128+
graphConfig={graphConfig}
129+
observabilitySessionId={observabilitySessionId}
130+
/>
131+
) : null}
141132

142133
{hasAgentFinalResponse ? (
143134
<Message icon={<ChatAgentAvatar />}>

coffeeAGNTCY/coffee_agents/lungo/frontend/src/components/Chat/ChatHeader.tsx

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
import React from "react"
77
import DeleteOutline from "@mui/icons-material/DeleteOutline"
8-
import UnfoldLess from "@mui/icons-material/UnfoldLess"
9-
import UnfoldMore from "@mui/icons-material/UnfoldMore"
108
import type { Theme } from "@mui/material/styles"
119
import { Box, IconButton, Tooltip } from "@open-ui-kit/core"
1210

@@ -29,19 +27,12 @@ function chatHeaderIconButtonSx(theme: Theme) {
2927
}
3028

3129
interface ChatHeaderProps {
32-
onMinimize?: () => void
3330
onClearConversation?: () => void
34-
isMinimized?: boolean
35-
/** ID of the expandable message panel controlled by the minimize button. */
36-
messagePanelId?: string
3731
horizontalPadding?: { xs: number; sm: number; md: number; lg: number }
3832
}
3933

4034
const ChatHeader: React.FC<ChatHeaderProps> = ({
41-
onMinimize,
4235
onClearConversation,
43-
isMinimized = false,
44-
messagePanelId,
4536
horizontalPadding = { xs: 2, sm: 4, md: 8, lg: 15 },
4637
}) => {
4738
return (
@@ -50,43 +41,22 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
5041
display: "flex",
5142
width: "100%",
5243
alignItems: "center",
53-
justifyContent: "space-between",
44+
justifyContent: "flex-end",
5445
px: horizontalPadding,
5546
py: 1,
5647
}}
5748
>
58-
<Box sx={{ display: "flex", alignItems: "center" }}>
59-
{onMinimize ? (
60-
<Tooltip title={isMinimized ? "Maximize" : "Minimize"}>
61-
<IconButton
62-
onClick={onMinimize}
63-
aria-label={isMinimized ? "Maximize" : "Minimize"}
64-
aria-expanded={!isMinimized}
65-
aria-controls={messagePanelId}
66-
sx={chatHeaderIconButtonSx}
67-
>
68-
{isMinimized ? (
69-
<UnfoldMore aria-hidden />
70-
) : (
71-
<UnfoldLess aria-hidden />
72-
)}
73-
</IconButton>
74-
</Tooltip>
75-
) : null}
76-
</Box>
77-
<Box sx={{ display: "flex", alignItems: "center" }}>
78-
{onClearConversation ? (
79-
<Tooltip title="Clear conversation">
80-
<IconButton
81-
onClick={onClearConversation}
82-
aria-label="Clear conversation"
83-
sx={chatHeaderIconButtonSx}
84-
>
85-
<DeleteOutline />
86-
</IconButton>
87-
</Tooltip>
88-
) : null}
89-
</Box>
49+
{onClearConversation ? (
50+
<Tooltip title="Clear conversation">
51+
<IconButton
52+
onClick={onClearConversation}
53+
aria-label="Clear conversation"
54+
sx={chatHeaderIconButtonSx}
55+
>
56+
<DeleteOutline />
57+
</IconButton>
58+
</Tooltip>
59+
) : null}
9060
</Box>
9161
)
9262
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright AGNTCY Contributors (https://github.com/agntcy)
3+
* SPDX-License-Identifier: Apache-2.0
4+
**/
5+
6+
import React from "react"
7+
8+
import ResizablePanelSeparator from "@/components/layout/ResizablePanelSeparator"
9+
10+
type ChatPanelSeparatorProps = {
11+
disabled?: boolean
12+
}
13+
14+
const ChatPanelSeparator: React.FC<ChatPanelSeparatorProps> = ({
15+
disabled = false,
16+
}) => (
17+
<ResizablePanelSeparator
18+
id="chat-panel-separator"
19+
aria-label="Resize agent chat"
20+
orientation="vertical"
21+
disabled={disabled}
22+
/>
23+
)
24+
25+
export default ChatPanelSeparator
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright AGNTCY Contributors (https://github.com/agntcy)
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Resizable graph/chat panel ids and size constraints (react-resizable-panels).
6+
*/
7+
8+
import {
9+
RESIZABLE_PANEL_MAX_SIZE,
10+
RESIZABLE_PANEL_MIN_SIZE,
11+
} from "@/components/layout/resizablePanelLayout"
12+
13+
export const MAIN_VERTICAL_GROUP_ID = "main-vertical"
14+
15+
export const GRAPH_PANEL_ID = "graph"
16+
export const CHAT_PANEL_ID = "chat"
17+
18+
/** Room for header, composer, and a short scrollable message area. */
19+
export const CHAT_MIN_SIZE = "12rem"
20+
21+
export const CHAT_MAX_SIZE = RESIZABLE_PANEL_MAX_SIZE
22+
23+
/** Keeps at least a quarter of the main column for the graph when chat is tall. */
24+
export const GRAPH_MIN_SIZE = RESIZABLE_PANEL_MIN_SIZE
25+
26+
export const CHAT_PANEL_AUTO_SIZE_MAX_ATTEMPTS = 10

0 commit comments

Comments
 (0)