@@ -5,16 +5,14 @@ import remarkGfm from "remark-gfm";
55import rehypePrism from "rehype-prism" ;
66import { AgentType , PlanChatProps , role } from "@/models" ;
77import {
8- Body1 ,
9- Button ,
10- Spinner ,
11- Tag ,
12- ToolbarDivider
8+ Body1 ,
9+ Button ,
10+ Spinner ,
11+ Tag ,
12+ ToolbarDivider ,
1313} from "@fluentui/react-components" ;
14- import {
15- HeartRegular ,
16- } from "@fluentui/react-icons" ;
17- import { useRef , useState } from "react" ;
14+ import { DiamondRegular , HeartRegular } from "@fluentui/react-icons" ;
15+ import { useEffect , useRef , useState } from "react" ;
1816import ReactMarkdown from "react-markdown" ;
1917import "../../styles/PlanChat.css" ;
2018import "../../styles/Chat.css" ;
@@ -23,105 +21,168 @@ import { TaskService } from "@/services/TaskService";
2321import InlineToaster from "../toast/InlineToaster" ;
2422
2523const PlanChat : React . FC < PlanChatProps > = ( {
26- planData,
27- input,
28- loading,
29- submittingChatDisableInput,
30- OnChatSubmit
31-
24+ planData,
25+ input,
26+ loading,
27+ submittingChatDisableInput,
28+ OnChatSubmit,
3229} ) => {
33- const messages = planData ?. messages || [ ] ;
34- const [ inputValue , setInput ] = useState ( input ) ;
35- const [ isTyping , setIsTyping ] = useState ( false ) ;
36- const [ showScrollButton , setShowScrollButton ] = useState ( false ) ;
37- const [ inputHeight , setInputHeight ] = useState ( 0 ) ;
38-
39- const messagesContainerRef = useRef < HTMLDivElement > ( null ) ;
40- const inputContainerRef = useRef < HTMLDivElement > ( null ) ;
41-
42-
43- const scrollToBottom = ( ) => { } ;
44- if ( ! planData ) return < Spinner size = "large" /> ;
45- return (
46- < div className = "chat-container" >
47- < div className = "messages" ref = { messagesContainerRef } >
48- < div className = "message-wrapper" >
49- { messages . map ( ( msg , index ) => {
50- const isHuman = msg . source === AgentType . HUMAN ;
51-
52- return (
53- < div key = { index } className = { `message ${ isHuman ? role . user : role . assistant } ` } >
54- { ! isHuman && (
55- < div className = "plan-chat-header" >
56- < div className = "plan-chat-speaker" >
57- < span className = "speaker-name" > { TaskService . cleanTextToSpaces ( msg . source ) } </ span >
58- < Tag size = "extra-small" shape = "rounded" appearance = "filled" className = "bot-tag" > BOT</ Tag >
59- </ div >
60- </ div >
61- ) }
62-
63- < Body1 >
64- < div className = "plan-chat-message-content" >
65- < ReactMarkdown remarkPlugins = { [ remarkGfm ] } rehypePlugins = { [ rehypePrism ] } >
66- { msg . content || "" }
67- </ ReactMarkdown >
68-
69- { ! isHuman && (
70- < div className = "assistant-footer" >
71- < div className = "assistant-actions" >
72- < Button
73- onClick = { ( ) => msg . content && navigator . clipboard . writeText ( msg . content ) }
74- title = "Copy Response"
75- appearance = "subtle"
76- style = { { height : 28 , width : 28 } }
77- icon = { < Copy /> }
78- />
79- < Tag size = "extra-small" > Sample data for demonstration purposes only.</ Tag >
80- </ div >
81- </ div >
82- ) }
83- </ div >
84- </ Body1 >
85- </ div >
86- ) ;
87- } ) }
88- </ div >
89-
90-
91- </ div >
92-
93- { showScrollButton && (
94- < Tag
95- onClick = { scrollToBottom }
96- className = "scroll-to-bottom plan-chat-scroll-button"
97- shape = "circular"
98- style = { { bottom : inputHeight } }
99- >
100- Back to bottom
101- </ Tag >
102- ) }
103- < InlineToaster />
104- < div ref = { inputContainerRef } className = "plan-chat-input-container" >
105- < div className = "plan-chat-input-wrapper" >
106- < ChatInput
107- value = { inputValue }
108- onChange = { setInput }
109- onEnter = { ( ) => OnChatSubmit ( inputValue ) }
110- disabledChat = { planData . enableChat ? submittingChatDisableInput : true }
111- placeholder = "Add more info to this task..."
30+ const messages = planData ?. messages || [ ] ;
31+ const [ inputValue , setInput ] = useState ( input ) ;
32+ const [ isTyping , setIsTyping ] = useState ( false ) ;
33+ const [ showScrollButton , setShowScrollButton ] = useState ( false ) ;
34+ const [ inputHeight , setInputHeight ] = useState ( 0 ) ;
35+
36+ const messagesContainerRef = useRef < HTMLDivElement > ( null ) ;
37+ const inputContainerRef = useRef < HTMLDivElement > ( null ) ;
38+
39+ // Scroll to Bottom useEffect
40+
41+ useEffect ( ( ) => {
42+ scrollToBottom ( ) ;
43+ } , [ messages ] ) ;
44+
45+ //Scroll to Bottom Buttom
46+
47+ useEffect ( ( ) => {
48+ const container = messagesContainerRef . current ;
49+ if ( ! container ) return ;
50+
51+ const handleScroll = ( ) => {
52+ const { scrollTop, scrollHeight, clientHeight } = container ;
53+ setShowScrollButton ( scrollTop + clientHeight < scrollHeight - 100 ) ;
54+ } ;
55+
56+ container . addEventListener ( "scroll" , handleScroll ) ;
57+ return ( ) => container . removeEventListener ( "scroll" , handleScroll ) ;
58+ } , [ ] ) ;
59+
60+ useEffect ( ( ) => {
61+ if ( inputContainerRef . current ) {
62+ setInputHeight ( inputContainerRef . current . offsetHeight ) ;
63+ }
64+ } , [ inputValue ] ) ; // or [inputValue, submittingChatDisableInput]
65+
66+
67+
68+ const scrollToBottom = ( ) => {
69+ messagesContainerRef . current ?. scrollTo ( {
70+ top : messagesContainerRef . current . scrollHeight ,
71+ behavior : "smooth" ,
72+ } ) ;
73+ setShowScrollButton ( false ) ;
74+ } ;
75+
76+ if ( ! planData ) return < Spinner size = "large" /> ;
77+ return (
78+ < div className = "chat-container" >
79+ < div className = "messages" ref = { messagesContainerRef } >
80+ < div className = "message-wrapper" >
81+ { messages . map ( ( msg , index ) => {
82+ const isHuman = msg . source === AgentType . HUMAN ;
83+
84+ return (
85+ < div
86+ key = { index }
87+ className = { `message ${ isHuman ? role . user : role . assistant } ` }
88+ >
89+ { ! isHuman && (
90+ < div className = "plan-chat-header" >
91+ < div className = "plan-chat-speaker" >
92+ < Body1 className = "speaker-name" >
93+ { TaskService . cleanTextToSpaces ( msg . source ) }
94+ </ Body1 >
95+ < Tag
96+ size = "extra-small"
97+ shape = "rounded"
98+ appearance = "brand"
99+ className = "bot-tag"
100+ >
101+ BOT
102+ </ Tag >
103+ </ div >
104+ </ div >
105+ ) }
106+
107+ < Body1 >
108+ < div className = "plan-chat-message-content" >
109+ < ReactMarkdown
110+ remarkPlugins = { [ remarkGfm ] }
111+ rehypePlugins = { [ rehypePrism ] }
112112 >
113- < Button
114- appearance = "transparent"
115- onClick = { ( ) => OnChatSubmit ( inputValue ) }
116- icon = { < Send /> }
117- disabled = { planData . enableChat ? submittingChatDisableInput : true }
118- />
119- </ ChatInput >
120- </ div >
121- </ div >
113+ { msg . content || "" }
114+ </ ReactMarkdown >
115+
116+ { ! isHuman && (
117+ < div className = "assistant-footer" >
118+ < div className = "assistant-actions" >
119+ < div >
120+ < Button
121+ onClick = { ( ) =>
122+ msg . content &&
123+ navigator . clipboard . writeText ( msg . content )
124+ }
125+ title = "Copy Response"
126+ appearance = "subtle"
127+ style = { { height : 28 , width : 28 } }
128+ icon = { < Copy /> }
129+ />
130+
131+ </ div >
132+
133+ < Tag icon = { < DiamondRegular /> } appearance = "filled" size = "extra-small" >
134+ Sample data for demonstration purposes only.
135+ </ Tag >
136+ </ div >
137+ </ div >
138+ ) }
139+ </ div >
140+ </ Body1 >
141+ </ div >
142+ ) ;
143+ } ) }
122144 </ div >
145+ </ div >
123146
124- ) ;
147+ { showScrollButton && (
148+ < Tag
149+ onClick = { scrollToBottom }
150+ className = "scroll-to-bottom plan-chat-scroll-button"
151+ shape = "circular"
152+ style = { {
153+ bottom : inputHeight ,
154+ position : "absolute" , // ensure this or your class handles it
155+ right : 16 , // optional, for right alignment
156+ zIndex : 5 ,
157+ } }
158+ >
159+ Back to bottom
160+ </ Tag >
161+
162+ ) }
163+ < InlineToaster />
164+ < div ref = { inputContainerRef } className = "plan-chat-input-container" >
165+ < div className = "plan-chat-input-wrapper" >
166+ < ChatInput
167+ value = { inputValue }
168+ onChange = { setInput }
169+ onEnter = { ( ) => OnChatSubmit ( inputValue ) }
170+ disabledChat = {
171+ planData . enableChat ? submittingChatDisableInput : true
172+ }
173+ placeholder = "Add more info to this task..."
174+ >
175+ < Button
176+ appearance = "transparent"
177+ onClick = { ( ) => OnChatSubmit ( inputValue ) }
178+ icon = { < Send /> }
179+ disabled = { planData . enableChat ? submittingChatDisableInput : true }
180+ />
181+ </ ChatInput >
182+ </ div >
183+ </ div >
184+ </ div >
185+ ) ;
125186} ;
126187
127188export default PlanChat ;
0 commit comments