Skip to content

Commit 9740eb0

Browse files
Merge pull request #40 from cortex-reply/feature/chat-ui
Feature/chat UI
2 parents a8dae75 + 5eb9e81 commit 9740eb0

4 files changed

Lines changed: 961 additions & 522 deletions

File tree

src/components/Chat/ChatInterfaceMessages.stories.tsx

Lines changed: 258 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useState, useEffect } from 'react'
22
import { Meta } from '@storybook/react'
33
import { ChatInterfaceMessages } from './ChatInterfaceMessages'
44
import { type ChatMessageProps } from './ChatMessage'
@@ -28,36 +28,99 @@ export const TextOnly: Story = {
2828
{
2929
id: '1',
3030
role: 'user',
31-
message: { type: 'text', data: { content: 'Hello!' } },
31+
message: { type: 'text', data: { content: 'Hello, I need help with my laptop.' } },
3232
},
3333
{
3434
id: '2',
3535
role: 'assistant',
36-
message: { type: 'text', data: { content: 'Hi there! How can I assist you today?' } },
37-
currentUser: { name: 'Robot' },
36+
message: { type: 'text', data: { content: 'Hi! I can help you with that. Can you describe the issue?' } },
37+
currentUser: { name: 'Helpdesk Bot' },
3838
},
3939
{
4040
id: '3',
4141
role: 'user',
42-
message: { type: 'text', data: { content: 'What is our leave policy?' } },
42+
message: { type: 'text', data: { content: 'It keeps restarting randomly.' } },
4343
},
4444
{
4545
id: '4',
4646
role: 'assistant',
47-
message: { type: 'forwarding', data: { content: 'HR' } },
47+
message: { type: 'forwarding', data: { content: 'IT Support' } },
4848
},
4949
{
5050
id: '5',
5151
role: 'assistant',
52-
message: { type: 'text', data: { content: 'Its blah blah blah' } },
52+
message: { type: 'text', data: { content: 'I have created a support ticket for you.' } },
5353
},
5454
{
5555
id: '6',
5656
role: 'assistant',
5757
message: {
5858
type: 'tool-invocation',
5959
data: {
60-
id: 1,
60+
id: 101,
61+
tool: 'task',
62+
system: 'payload',
63+
fetchLatest: false,
64+
taskData: chatCardTestData,
65+
},
66+
},
67+
},
68+
{
69+
id: '7',
70+
role: 'user',
71+
message: { type: 'text', data: { content: 'Thank you! Can I also get help with my printer?' } },
72+
},
73+
{
74+
id: '8',
75+
role: 'assistant',
76+
message: { type: 'text', data: { content: 'Of course! What issue are you facing with the printer?' } },
77+
},
78+
{
79+
id: '9',
80+
role: 'user',
81+
message: { type: 'text', data: { content: 'It is not connecting to the network.' } },
82+
},
83+
{
84+
id: '10',
85+
role: 'assistant',
86+
message: { type: 'forwarding', data: { content: 'Printer Support' } },
87+
},
88+
{
89+
id: '11',
90+
role: 'assistant',
91+
message: { type: 'text', data: { content: 'A new ticket has been created for your printer issue.' } },
92+
},
93+
{
94+
id: '12',
95+
role: 'assistant',
96+
message: {
97+
type: 'tool-invocation',
98+
data: {
99+
id: 102,
100+
tool: 'task',
101+
system: 'payload',
102+
fetchLatest: false,
103+
taskData: chatCardTestData,
104+
},
105+
},
106+
},
107+
{
108+
id: '13',
109+
role: 'user',
110+
message: { type: 'text', data: { content: 'How do I check the status of my tickets?' } },
111+
},
112+
{
113+
id: '14',
114+
role: 'assistant',
115+
message: { type: 'text', data: { content: 'You can view all your open tickets in the Helpdesk portal.' } },
116+
},
117+
{
118+
id: '15',
119+
role: 'assistant',
120+
message: {
121+
type: 'tool-invocation',
122+
data: {
123+
id: 103,
61124
tool: 'task',
62125
system: 'payload',
63126
fetchLatest: false,
@@ -76,3 +139,190 @@ export const TextOnly: Story = {
76139
},
77140
},
78141
}
142+
143+
export const StreamingChunks: Story = {
144+
render: (args) => {
145+
const [messages, setMessages] = useState<ChatMessageProps[]>([])
146+
useEffect(() => {
147+
const convo: ChatMessageProps[] = [
148+
{
149+
id: '1',
150+
role: 'user',
151+
message: { type: 'text', data: { content: 'Hello, I need help with my laptop.' } },
152+
},
153+
{
154+
id: '2',
155+
role: 'assistant',
156+
message: { type: 'text', data: { content: '' } },
157+
currentUser: { name: 'Helpdesk Bot' },
158+
},
159+
{
160+
id: '3',
161+
role: 'user',
162+
message: { type: 'text', data: { content: 'It keeps restarting randomly.' } },
163+
},
164+
{
165+
id: '4',
166+
role: 'assistant',
167+
message: { type: 'forwarding', data: { content: 'IT Support' } },
168+
},
169+
{
170+
id: '5',
171+
role: 'assistant',
172+
message: { type: 'text', data: { content: '' } },
173+
},
174+
{
175+
id: '6',
176+
role: 'assistant',
177+
message: {
178+
type: 'tool-invocation',
179+
data: {
180+
id: 101,
181+
tool: 'task',
182+
system: 'payload',
183+
fetchLatest: false,
184+
taskData: chatCardTestData,
185+
},
186+
},
187+
},
188+
{
189+
id: '7',
190+
role: 'user',
191+
message: { type: 'text', data: { content: 'Thank you! Can I also get help with my printer?' } },
192+
},
193+
{
194+
id: '8',
195+
role: 'assistant',
196+
message: { type: 'text', data: { content: '' } },
197+
},
198+
{
199+
id: '9',
200+
role: 'user',
201+
message: { type: 'text', data: { content: 'It is not connecting to the network.' } },
202+
},
203+
{
204+
id: '10',
205+
role: 'assistant',
206+
message: { type: 'forwarding', data: { content: 'Printer Support' } },
207+
},
208+
{
209+
id: '11',
210+
role: 'assistant',
211+
message: { type: 'text', data: { content: '' } },
212+
},
213+
{
214+
id: '12',
215+
role: 'assistant',
216+
message: {
217+
type: 'tool-invocation',
218+
data: {
219+
id: 102,
220+
tool: 'task',
221+
system: 'payload',
222+
fetchLatest: false,
223+
taskData: chatCardTestData,
224+
},
225+
},
226+
},
227+
{
228+
id: '13',
229+
role: 'user',
230+
message: { type: 'text', data: { content: 'How do I check the status of my tickets?' } },
231+
},
232+
{
233+
id: '14',
234+
role: 'assistant',
235+
message: { type: 'text', data: { content: '' } },
236+
},
237+
{
238+
id: '15',
239+
role: 'assistant',
240+
message: {
241+
type: 'tool-invocation',
242+
data: {
243+
id: 103,
244+
tool: 'task',
245+
system: 'payload',
246+
fetchLatest: false,
247+
taskData: chatCardTestData,
248+
},
249+
},
250+
},
251+
]
252+
// Split assistant text messages into word chunks
253+
const assistantTexts = [
254+
'Hi! I can help you with that. Can you describe the issue?', // id:2
255+
'I have created a support ticket for you.', // id:5
256+
'Of course! What issue are you facing with the printer?', // id:8
257+
'A new ticket has been created for your printer issue.', // id:11
258+
'You can view all your open tickets in the Helpdesk portal.', // id:14
259+
]
260+
const assistantChunks = assistantTexts.map(text => text.split(/(\s+)/).filter(Boolean))
261+
let idx = 0
262+
let chunkIdx = 0
263+
let msgIdx = 0
264+
let timer: NodeJS.Timeout | null = null
265+
setMessages([convo[0]])
266+
function streamNext(currentMessages: ChatMessageProps[]) {
267+
if (idx >= convo.length) return
268+
// If this is a streaming assistant text message
269+
if ([1,4,7,10,13].includes(idx)) {
270+
// Add the message with empty content if not already present
271+
if (currentMessages.length < idx + 1) {
272+
setMessages((prev) => {
273+
const next = [...prev, convo[idx]]
274+
timer = setTimeout(() => streamNext(next), 1000)
275+
return next
276+
})
277+
return
278+
}
279+
// Stream word chunks for this message
280+
if (chunkIdx < assistantChunks[msgIdx].length) {
281+
setMessages((prev) => {
282+
const updated = [...prev]
283+
const last = { ...updated[idx] }
284+
last.message = {
285+
...last.message,
286+
data: {
287+
...last.message.data,
288+
content: (last.message.data?.content || '') + assistantChunks[msgIdx][chunkIdx],
289+
},
290+
}
291+
updated[idx] = last
292+
timer = setTimeout(() => streamNext(updated), 200)
293+
return updated
294+
})
295+
chunkIdx++
296+
return
297+
} else {
298+
chunkIdx = 0
299+
msgIdx++
300+
idx++
301+
timer = setTimeout(() => streamNext(messages), 1000)
302+
return
303+
}
304+
}
305+
// Add the next message
306+
setMessages((prev) => {
307+
const next = [...prev, convo[idx]]
308+
timer = setTimeout(() => streamNext(next), 1000)
309+
return next
310+
})
311+
idx++
312+
}
313+
timer = setTimeout(() => streamNext([convo[0]]), 1000)
314+
return () => { if (timer) clearTimeout(timer) }
315+
}, [])
316+
return <ChatInterfaceMessages {...args} messages={messages} />
317+
},
318+
args: {
319+
contextType: 'project',
320+
currentUser: {
321+
id: 1,
322+
name: 'John Doe',
323+
email: 'john.doe@example.com',
324+
avatar: '/path-to-avatar.jpg',
325+
role: 'user',
326+
},
327+
},
328+
}

src/components/Chat/ChatInterfaceMessages.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,18 @@ interface ChatInterfaceProps {
3636
* Custom Chat interface component
3737
*/
3838
export function ChatInterfaceMessages({
39-
messages: initialMessages,
40-
contextType: contextType,
41-
currentUser,
42-
businessFunctions,
39+
messages,
40+
isLoading,
4341
}: ChatInterfaceProps) {
44-
// State for chat messages
45-
const [messages, setMessages] = useState<ChatMessageProps[]>(initialMessages);
46-
// State for input field
47-
const [input, setInput] = useState("");
48-
// State for loading status
49-
const [isLoading, setIsLoading] = useState(false);
50-
const scrollAreaRef = useRef<HTMLDivElement>(null)
51-
const [selectedFunction, setSelectedFunction] = useState<number | null>(null)
52-
53-
// Scroll to bottom when messages change
54-
useEffect(() => {
55-
if (scrollAreaRef.current) {
56-
const scrollContainer = scrollAreaRef.current.querySelector(
57-
"[data-radix-scroll-area-viewport]"
58-
);
59-
if (scrollContainer) {
60-
scrollContainer.scrollTop = scrollContainer.scrollHeight;
61-
}
62-
}
63-
}, [messages]);
64-
65-
42+
const messagesEndRef = useRef<HTMLDivElement>(null);
6643

44+
useEffect(() => {
45+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
46+
}, [messages]);
47+
6748
return (
6849
<div className="flex flex-col h-full overflow-hidden">
69-
<ScrollArea className="container flex-1 p-4" ref={scrollAreaRef}>
50+
<ScrollArea className="container flex-1 p-4">
7051
<div className="space-y-6">
7152
{messages.length === 0 && (
7253
<div className="flex items-center justify-center h-full pt-60">
@@ -104,6 +85,7 @@ useEffect(() => {
10485
<ChatMessage key={index} {...message} />
10586
))}
10687
</div>
88+
<div ref={messagesEndRef} />
10789
</ScrollArea>
10890
</div>
10991
)

src/components/Chat/ChatMessage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export interface ChatMessageProps {
2828
}
2929
}
3030

31-
export const ChatMessage: React.FC<ChatMessageProps> = ({ id, message, currentUser, role }) => {
31+
export const ChatMessage = React.memo(function ChatMessage(props: ChatMessageProps) {
32+
const { id, message, currentUser, role } = props
3233
const isUser = role === 'user'
3334
const userName = isUser ? currentUser?.name || 'You' : currentUser?.name || 'AI Assistant'
3435
const userAvatar = isUser
@@ -158,4 +159,4 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ id, message, currentUs
158159
// </div>
159160
// </div>
160161
// );
161-
}
162+
})

0 commit comments

Comments
 (0)