@@ -10,28 +10,42 @@ import { VueLenis } from 'lenis/vue'
1010import { Skeleton } from ' @/lib/shadcn/components/ui/skeleton'
1111import { Card , CardContent } from ' ~/lib/shadcn/components/ui/card'
1212import VanishingInput from ' ~/lib/shadcn/components/ui/vanishing-input/VanishingInput.vue'
13+ import LiquidGlassDiv from ' ../LiquidGlassDiv.vue'
1314
15+ const isDev = import .meta .dev
1416const { $auth } = useNuxtApp ()
1517const { convexApiUrl } = useRuntimeConfig ().public
1618const convex = useConvexClient ()
19+ const chatContext = useChatContext ()
1720
18- // For [...all] routing the value is an array
19- const threadIdRef = useRouteParams <string >(' all' , undefined , { transform: { get : s => Array .isArray (s ) ? s [0 ] : s } })
21+ // Lenis have bug with useTemplateRef
22+ const lenisRef = ref <{ $el: HTMLElement , lenis: Lenis }>()
23+ const { y : scrollY } = useScroll (computed (() => lenisRef .value ?.$el ))
24+ const nearTopBottom = computed (() => {
25+ const el = lenisRef .value ?.$el
26+ if (! el )
27+ return [null , null ]
28+
29+ const currentScroll = lenisRef .value ! .lenis .targetScroll || scrollY .value
30+
31+ const gapFromTop = currentScroll
32+ const gapFromBottom = el .scrollHeight - el .clientHeight - currentScroll
33+
34+ const nearTop = gapFromTop < 69
35+ const nearBottom = gapFromBottom < 69
36+ return [nearTop && gapFromTop + 1 , nearBottom && gapFromBottom + 1 , lenisRef .value ! .lenis .targetScroll , scrollY .value ]
37+ })
38+
39+ const threadIdRef = useThreadIdRef ()
2040
2141const cachedThreadsMessages: {
2242 [threadId : string ]: Array <CustomMessage >
2343} = {}
24-
2544const messages = ref <Array <CustomMessage >>([])
2645const messagesMapped = computed (() => keyBy (messages .value , ' id' ))
2746const streamingMessages = ref (0 )
28-
29- const chatInput = ref (' ' )
30-
3147const isFetching = ref (false )
32-
33- // Lenis have bug with useTemplateRef
34- const lenisRef = ref <{ $el: HTMLElement , lenis: Lenis }>()
48+ const chatInput = ref (' ' )
3549
3650// Fetch messages and resume streams
3751const { ignoreUpdates : ignorePathUpdate } = watchIgnorable (
@@ -73,7 +87,7 @@ const { ignoreUpdates: ignorePathUpdate } = watchIgnorable(
7387 }
7488 }
7589
76- doScrollBottom ({ forceTries: 3 } )
90+ nextTick (() => doScrollBottom ({ tries: 6 }) )
7791 }
7892 },
7993 { immediate: true },
@@ -107,7 +121,7 @@ async function handleSubmit({ input, confirmMultiStream = false }: HandleSubmitA
107121
108122 chatInput .value = ' '
109123
110- nextTick (() => { doScrollBottom () })
124+ nextTick (() => { doScrollBottom ({ tries: 2 } ) })
111125
112126 // Create new thread
113127 if (! threadIdRef .value ) {
@@ -177,8 +191,7 @@ async function pollToMessage({ message, resumeStreamId, threadId = threadIdRef.v
177191 console .log (' Poll completed' )
178192 }
179193
180- if (isNearBottom ())
181- setTimeout (() => { doScrollBottom ({ forceTries: 1 }) }, 100 )
194+ nextTick (() => { doScrollBottom ({ maybe: true }) })
182195}
183196
184197interface StreamToMessageArgs {
@@ -266,8 +279,7 @@ async function streamToMessage({ message, content, resumeStreamId }: StreamToMes
266279 if (state .content )
267280 message .content += state .content
268281
269- if (isNearBottom ())
270- setTimeout (() => { doScrollBottom ({ forceTries: 1 }) }, 100 )
282+ nextTick (() => { doScrollBottom ({ maybe: true }) })
271283 }
272284
273285 message .isStreaming = false
@@ -283,45 +295,38 @@ async function streamToMessage({ message, content, resumeStreamId }: StreamToMes
283295 console .log (' Stream completed' )
284296}
285297
286- function doScrollBottom({ smooth = true , maybe = false , forceTries = 0 , lastScrollTop = 0 } = {}) {
298+ function doScrollBottom({ smooth = true , maybe = false , tries = 0 , lastScrollTop = 0 } = {}) {
287299 if (! lenisRef .value )
288300 return
289301
290302 const l = lenisRef .value
291303 const scrollHeight = l .$el .scrollHeight
292304
293- if (l .$el .scrollTop < lastScrollTop )
294- forceTries = 0
305+ // Allow user to try escape the tries
306+ if (tries && (l .$el .scrollTop < lastScrollTop ))
307+ tries = 0
295308
296- if (! forceTries && maybe && ! isNearBottom () )
309+ if (maybe && l . lenis . direction !== 1 )
297310 return
298311
299- if (scrollHeight !== l .lenis .limit + l .$el .clientHeight )
312+ if (scrollHeight !== l .lenis .limit + l .$el .clientHeight ) {
300313 l .lenis .resize ()
314+ ++ tries
315+ }
301316
302317 smooth
303318 ? l .lenis .scrollTo (scrollHeight )
304319 : l .$el .scrollTop = scrollHeight
305320
306321 lastScrollTop = l .$el .scrollTop
307322
308- if (forceTries ) {
309- countdown (200 , () => {
310- sleep (0 ).then (() => doScrollBottom ({ smooth , maybe , forceTries: forceTries - 1 , lastScrollTop }))
323+ if (tries > 1 ) {
324+ countdown (250 , () => {
325+ sleep (0 ).then (() => doScrollBottom ({ smooth , maybe , tries: tries - 1 , lastScrollTop }))
311326 }, { key: ' dSB' , replace: true })
312327 }
313328}
314329
315- function isNearBottom() {
316- if (! lenisRef .value )
317- return
318-
319- const l = lenisRef .value
320- const scrollHeight = l .$el .scrollHeight
321-
322- return (l .$el .scrollTop + l .$el .clientHeight ) > (scrollHeight - 69 )
323- }
324-
325330const multiStreamConfirmDialogOpen = ref (false )
326331let savedChatInput = ' '
327332function alertIsStreaming(input : string ) {
@@ -334,15 +339,16 @@ function alertIsStreaming(input: string) {
334339 <div class =" relative flex flex-col" >
335340 <VueLenis ref =" lenisRef" class =" h-screen overflow-y-scroll px-4" >
336341 <div class =" mx-auto h-full max-w-full lg:max-w-4xl" >
342+ <FlickeringGrid
343+ v-if =" chatContext.insaneUI.value"
344+ class =" absolute inset-0 z-0 place-content-center" :square-size =" 10" :grid-gap =" 5"
345+ color =" #60A5FA" :max-opacity =" 0.5" :flicker-chance =" 0.1"
346+ />
347+
337348 <div
338349 v-show =" !(isFetching && !messages.length)"
339- class =" pointer-events-none absolute left-0 h-screen w-full place-content-center overflow-hidden"
350+ class =" pointer-events-none absolute left-0 z-0 h-screen w-full place-content-center overflow-hidden"
340351 >
341- <FlickeringGrid
342- v-if =" false" class =" absolute inset-0 z-0 place-content-center" :square-size =" 4" :grid-gap =" 6"
343- color =" #60A5FA" :max-opacity =" 0.5" :flicker-chance =" 0.1" :width =" 2000" :height =" 2000"
344- />
345-
346352 <div
347353 v-if =" !messages.length"
348354 class =" relative z-2 whitespace-pre-wrap px-2 text-center text-4xl text-gray-400 font-medium tracking-tighter dark:text-gray-500 dark:text-white"
@@ -353,48 +359,73 @@ function alertIsStreaming(input: string) {
353359 </div >
354360 </div >
355361
356- <div v-if =" messages.length" class =" z-2 space-y-4" >
362+ <div v-if =" messages.length" class =" relative z-2 space-y-4" >
357363 <div class =" pt-6" />
358364
359365 <div
360366 v-for =" m of messages" :key =" m.id" class =" flex"
361367 :class =" m.role === 'user' ? 'justify-end' : 'justify-start'"
362368 >
363- <Card
364- class =" bg-transparent shadow-md"
369+ <component
370+ :is =" chatContext.insaneUI.value ? LiquidGlassDiv : 'div'"
371+ class =" rounded-$radius $c-radius=$radius"
365372 :class =" [
366- m.role === 'user' ? 'border-secondary-200' : 'border-primary-200',
367- m.role === 'user' ? 'max-w-80% md:max-w-2xl' : 'max-w-full md:max-w-3xl',
373+ m.role === 'user'
374+ ? 'bg-secondary-100 dark:bg-secondary-950'
375+ : 'bg-primary-100 dark:bg-primary-950',
376+ chatContext.insaneUI.value
377+ ? 'bg-opacity-50!'
378+ : 'bg-opacity-5!',
368379 ]"
369380 >
370- <!-- <CardHeader class="px-4 py-2">
371- <CardTitle class="text-sm font-semibold">
372- {{ m.role === 'user' ? $t('pages.chat.userLabel') : $t('pages.chat.aiLabel') }}
373- </CardTitle>
374- </CardHeader> -->
375- <CardContent class =" px-4 py-3" >
376- <div v-if =" m.isStreaming && !m.content" class =" flex gap-2" >
377- <div >{{ $t('generating') }}</div >
378- <div class =" spinner h-5 w-5" />
379- </div >
380- <MDC v-else :value =" m.content" />
381- <div class =" hidden first:block" >
382- <Skeleton
383- class =" h-5 w-$c-W rounded-full bg-muted-foreground" :style =" {
384- '--c-W': `${(Math.floor(Math.random() * (300 - 100 + 1)) + 100) * (m.role === 'user' ? 1 : 2)}px`,
385- }"
386- />
387- </div >
388- </CardContent >
389- </Card >
381+ <Card
382+ class =" bg-transparent shadow-md"
383+ :class =" [
384+ m.role === 'user'
385+ ? 'border-secondary-200 max-w-80% md:max-w-2xl'
386+ : 'border-primary-200 max-w-full md:max-w-3xl',
387+ ]"
388+ >
389+ <!-- <CardHeader class="px-4 py-2">
390+ <CardTitle class="text-sm font-semibold">
391+ {{ m.role === 'user' ? $t('pages.chat.userLabel') : $t('pages.chat.aiLabel') }}
392+ </CardTitle>
393+ </CardHeader> -->
394+ <CardContent class =" min-w-25 px-4 py-3 [& _.prose-hr]:(border-accent-foreground!)" >
395+ <div v-if =" m.isStreaming && !m.content" class =" my-4 flex gap-2" >
396+ <div >{{ $t('generating') }}</div >
397+ <div class =" spinner h-5 w-5" />
398+ </div >
399+ <MDC v-else :value =" m.content" />
400+ <div class =" my-4 hidden first:block" >
401+ <Skeleton
402+ class =" h-5 w-$c-W rounded-full bg-muted-foreground" :style =" {
403+ '--c-W': `${(Math.floor(Math.random() * (300 - 100 + 1)) + 100) * (m.role === 'user' ? 1 : 2)}px`,
404+ }"
405+ />
406+ </div >
407+ </CardContent >
408+ </Card >
409+ </component >
390410 </div >
391411
392412 <div class =" pb-40" />
393413 </div >
394414 </div >
395415 </VueLenis >
396416
397- <LiquidGlassDiv class =" bottom-0 left-0 max-w-full w-full border-t border-secondary p-4 $c-radius=0px absolute!" >
417+ <LiquidGlassDiv class =" bottom-0 left-0 z-3 max-w-full w-full border-t border-secondary p-4 $c-radius=0px absolute!" >
418+ <div v-if =" isDev" class =" absolute bottom-100%" >
419+ {{ nearTopBottom }}
420+ </div >
421+ <div class =" absolute bottom-100% right-6 mb-2 flex flex-col gap-2" >
422+ <Button variant =" outline" size =" icon" class =" rounded-xl p-1 opacity-100 transition-opacity duration-500" :class =" nearTopBottom[0] && 'invisible opacity-0'" @click =" lenisRef!.lenis.scrollTo('top')" >
423+ <div class =" i-hugeicons:circle-arrow-up-03 h-full w-full" />
424+ </Button >
425+ <Button variant =" outline" size =" icon" class =" rounded-xl p-1 opacity-100 transition-opacity duration-500" :class =" nearTopBottom[1] && 'invisible opacity-0'" @click =" lenisRef!.lenis.scrollTo('bottom')" >
426+ <div class =" i-hugeicons:circle-arrow-down-03 h-full w-full" />
427+ </Button >
428+ </div >
398429 <div >
399430 <div class =" mx-auto max-w-lg flex flex-col gap-3" >
400431 <VanishingInput
0 commit comments