@@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
99import {
1010 type AIChatController ,
1111 type AIChatState ,
12+ useAI ,
1213 useAIChatController ,
1314 useAIChatState ,
1415} from '../AI' ;
@@ -24,15 +25,15 @@ import {
2425import { useTrackEvent } from '../Insights' ;
2526import { useNow } from '../hooks' ;
2627import { Button } from '../primitives' ;
28+ import { ScrollContainer } from '../primitives/ScrollContainer' ;
2729import { AIChatControlButton } from './AIChatControlButton' ;
2830import { AIChatIcon } from './AIChatIcon' ;
2931import { AIChatInput } from './AIChatInput' ;
3032import { AIChatMessages } from './AIChatMessages' ;
3133import AIChatSuggestedQuestions from './AIChatSuggestedQuestions' ;
3234
33- export function AIChat ( props : { trademark : boolean } ) {
34- const { trademark } = props ;
35-
35+ export function AIChat ( ) {
36+ const { config } = useAI ( ) ;
3637 const language = useLanguage ( ) ;
3738 const chat = useAIChatState ( ) ;
3839 const chatController = useAIChatController ( ) ;
@@ -70,18 +71,18 @@ export function AIChat(props: { trademark: boolean }) {
7071 < div
7172 data-testid = "ai-chat"
7273 className = { tcls (
73- 'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 depth-flat: lg:p-0 lg:pr-4 lg:pl -0 xl:w-96' ,
74+ 'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96' ,
7475 chat . opened
7576 ? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0'
7677 : 'hidden lg:ml-0 lg:w-0! lg:opacity-0'
7778 ) }
7879 >
79- < EmbeddableFrame className = "relative shrink-0 circular-corners:rounded-3xl rounded-corners:rounded-md border border -tint-subtle depth-subtle:shadow-lg shadow -tint transition-all duration-300 lg:w-76 depth-flat:lg: rounded-none depth-flat: lg:border-y-0 depth-flat: lg:border-r-0 xl:w-92 " >
80+ < EmbeddableFrame className = "relative shrink-0 border-tint-subtle border-l to -tint-base transition-all duration-300 max- lg:circular-corners: rounded-3xl max- lg:rounded-corners:rounded-md max- lg:border lg:w-80 xl:w-96 " >
8081 < EmbeddableFrameHeader >
81- < AIChatDynamicIcon trademark = { trademark } />
82+ < AIChatDynamicIcon trademark = { config . trademark } />
8283 < EmbeddableFrameHeaderMain >
8384 < EmbeddableFrameTitle >
84- { getAIChatName ( language , trademark ) }
85+ { getAIChatName ( language , config . trademark ) }
8586 </ EmbeddableFrameTitle >
8687 < AIChatSubtitle chat = { chat } />
8788 </ EmbeddableFrameHeaderMain >
@@ -98,7 +99,7 @@ export function AIChat(props: { trademark: boolean }) {
9899 </ EmbeddableFrameButtons >
99100 </ EmbeddableFrameHeader >
100101 < EmbeddableFrameBody >
101- < AIChatBody chatController = { chatController } chat = { chat } trademark = { trademark } />
102+ < AIChatBody chatController = { chatController } chat = { chat } />
102103 </ EmbeddableFrameBody >
103104 </ EmbeddableFrame >
104105 </ div >
@@ -145,10 +146,33 @@ export function AIChatSubtitle(props: {
145146 const language = useLanguage ( ) ;
146147
147148 return (
148- < EmbeddableFrameSubtitle className = { chat . loading ? 'h-3 opacity-11' : 'h-0 opacity-0' } >
149- { chat . messages [ chat . messages . length - 1 ] ?. content
150- ? tString ( language , 'ai_chat_working' )
151- : tString ( language , 'ai_chat_thinking' ) }
149+ < EmbeddableFrameSubtitle
150+ className = { tcls ( 'relative' , chat . loading ? 'h-3 opacity-11' : 'h-0 opacity-0' ) }
151+ >
152+ < span
153+ className = { tcls (
154+ 'absolute left-0' ,
155+ chat . loading
156+ ? chat . messages [ chat . messages . length - 1 ] ?. content
157+ ? 'animate-blur-in-slow'
158+ : 'hidden'
159+ : 'animate-blur-out-slow'
160+ ) }
161+ >
162+ { t ( language , 'ai_chat_working' ) }
163+ </ span >
164+ < span
165+ className = { tcls (
166+ 'absolute left-0' ,
167+ chat . loading
168+ ? chat . messages [ chat . messages . length - 1 ] ?. content
169+ ? 'animate-blur-out-slow'
170+ : 'animate-blur-in-slow'
171+ : 'hidden'
172+ ) }
173+ >
174+ { t ( language , 'ai_chat_thinking' ) }
175+ </ span >
152176 </ EmbeddableFrameSubtitle >
153177 ) ;
154178}
@@ -159,20 +183,13 @@ export function AIChatSubtitle(props: {
159183export function AIChatBody ( props : {
160184 chatController : AIChatController ;
161185 chat : AIChatState ;
162- trademark : boolean ;
163186 welcomeMessage ?: string ;
164187 suggestions ?: string [ ] ;
165188} ) {
166- const { chatController, chat, trademark, suggestions } = props ;
189+ const { chatController, chat, suggestions } = props ;
190+ const { trademark } = useAI ( ) . config ;
167191
168192 const [ input , setInput ] = React . useState ( '' ) ;
169-
170- const scrollContainerRef = React . useRef < HTMLDivElement > ( null ) ;
171- // Ref for the last user message element
172- const lastUserMessageRef = React . useRef < HTMLDivElement > ( null ) ;
173- const inputRef = React . useRef < HTMLDivElement > ( null ) ;
174-
175- const [ inputHeight , setInputHeight ] = React . useState ( 0 ) ;
176193 const language = useLanguage ( ) ;
177194 const now = useNow ( 60 * 60 * 1000 ) ; // Refresh every hour for greeting
178195
@@ -186,67 +203,42 @@ export function AIChatBody(props: {
186203 return tString ( language , 'ai_chat_assistant_greeting_evening' ) ;
187204 } , [ now , language ] ) ;
188205
189- // Auto-scroll to the latest user message when messages change
190- React . useEffect ( ( ) => {
191- if ( chat . messages . length > 0 && lastUserMessageRef . current ) {
192- lastUserMessageRef . current . scrollIntoView ( {
193- behavior : 'smooth' ,
194- block : 'start' ,
195- } ) ;
196- }
197- } , [ chat . messages . length ] ) ;
198-
199- React . useEffect ( ( ) => {
200- const timeout = setTimeout ( ( ) => {
201- if ( lastUserMessageRef . current ) {
202- lastUserMessageRef . current . scrollIntoView ( {
203- behavior : 'smooth' ,
204- block : 'start' ,
205- } ) ;
206- }
207- } , 100 ) ;
208-
209- // We want the chat messages to scroll underneath the input, but they should scroll past the input when scrolling all the way down.
210- // The best way to do this is to observe the input height and adjust the padding bottom of the scroll container accordingly.
211- const observer = new ResizeObserver ( ( entries ) => {
212- entries . forEach ( ( entry ) => {
213- setInputHeight ( entry . contentRect . height + 32 ) ;
214- } ) ;
215- } ) ;
216- if ( inputRef . current ) {
217- observer . observe ( inputRef . current ) ;
218- }
219- return ( ) => {
220- observer . disconnect ( ) ;
221- clearTimeout ( timeout ) ;
222- } ;
223- } , [ ] ) ;
224-
225206 return (
226207 < >
227- < div
228- ref = { scrollContainerRef }
229- className = " gutter-stable flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p -4"
230- style = { {
231- paddingBottom : ` ${ inputHeight } px` ,
232- } }
208+ < ScrollContainer
209+ className = "shrink grow basis-80 animate-fade-in-slow [container-type:size]"
210+ contentClassName = "p-4 gutter-stable flex flex-col gap-4"
211+ orientation = "vertical"
212+ fadeEdges = { [ 'leading' ] }
213+ active = { `message-group- ${ chat . messages . filter ( ( message ) => message . role === 'user' ) . length - 1 } ` }
233214 >
234215 { isEmpty ? (
235- < div className = "flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4" >
236- < div className = "flex size-32 animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle" >
237- < AIChatIcon
238- state = "intro"
239- trademark = { trademark }
240- className = "size-16 animate-[present_500ms_200ms_both]"
241- />
242- </ div >
243- < div className = "animate-[fadeIn_500ms_400ms_both]" >
244- < h5 className = " text-center font-bold text-lg text-tint-strong" >
245- { timeGreeting }
246- </ h5 >
247- < p className = "text-center text-tint" >
248- { t ( language , 'ai_chat_assistant_description' ) }
249- </ p >
216+ < div className = "flex grow flex-col" >
217+ < div className = "my-auto flex flex-row items-center gap-4 pb-6 [@container(min-height:400px)]:flex-col" >
218+ < div
219+ className = "flex size-16 shrink-0 animate-scale-in items-center justify-center rounded-full bg-primary-solid/1 [@container(min-height:400px)]:size-32"
220+ style = { { animationDelay : '.3s' } }
221+ >
222+ < AIChatIcon
223+ state = "intro"
224+ trademark = { trademark }
225+ className = "size-8 text-primary [@container(min-height:400px)]:size-16"
226+ />
227+ </ div >
228+ < div className = "flex flex-col items-start [@container(min-height:400px)]:items-center" >
229+ < h5
230+ className = "animate-blur-in-slow font-bold text-lg text-tint-strong [@container(min-height:400px)]:text-center"
231+ style = { { animationDelay : '.5s' } }
232+ >
233+ { timeGreeting }
234+ </ h5 >
235+ < p
236+ className = "animate-blur-in-slow text-tint [@container(min-height:400px)]:text-center"
237+ style = { { animationDelay : '.6s' } }
238+ >
239+ { t ( language , 'ai_chat_assistant_description' ) }
240+ </ p >
241+ </ div >
250242 </ div >
251243 { ! chat . error ? (
252244 < AIChatSuggestedQuestions
@@ -256,17 +248,11 @@ export function AIChatBody(props: {
256248 ) : null }
257249 </ div >
258250 ) : (
259- < AIChatMessages
260- chat = { chat }
261- chatController = { chatController }
262- lastUserMessageRef = { lastUserMessageRef }
263- />
251+ < AIChatMessages chat = { chat } chatController = { chatController } />
264252 ) }
265- </ div >
266- < div
267- ref = { inputRef }
268- className = "absolute inset-x-0 bottom-0 mr-2 flex select-none flex-col gap-4 bg-linear-to-b from-transparent to-50% to-tint-base/9 p-4 pr-2"
269- >
253+ </ ScrollContainer >
254+
255+ < div className = "flex flex-col gap-2 px-4 pb-4" >
270256 { /* Display an error banner when something went wrong. */ }
271257 { chat . error ? < AIChatError chatController = { chatController } /> : null }
272258
0 commit comments