1
1
'use client' ;
2
-
3
2
import { tcls } from '@/lib/tailwind' ;
4
3
import { Icon , type IconName } from '@gitbook/icons' ;
4
+ import Link from 'next/link' ;
5
5
import { useEffect } from 'react' ;
6
6
import { useState } from 'react' ;
7
7
import { useVisitedPages } from '../Insights' ;
8
8
import { usePageContext } from '../PageContext' ;
9
9
import { streamPageJourneySuggestions } from './server-actions' ;
10
10
11
+ const JOURNEY_COUNT = 4 ;
12
+
11
13
export function AIPageJourneySuggestions ( props : { spaces : { id : string ; title : string } [ ] } ) {
12
14
const { spaces } = props ;
13
15
14
16
const currentPage = usePageContext ( ) ;
15
17
16
18
// const language = useLanguage();
17
19
const visitedPages = useVisitedPages ( ( state ) => state . pages ) ;
18
- const [ journeys , setJourneys ] = useState < ( { label ?: string ; icon ?: string } | undefined ) [ ] > ( [ ] ) ;
20
+ const [ journeys , setJourneys ] = useState <
21
+ Array < {
22
+ label : string ;
23
+ icon ?: string ;
24
+ pages ?: Array < {
25
+ id : string ;
26
+ title : string ;
27
+ href : string ;
28
+ icon ?: string ;
29
+ emoji ?: string ;
30
+ } > ;
31
+ } >
32
+ > ( Array . from ( { length : JOURNEY_COUNT } ) ) ;
33
+ const [ selected , setSelected ] = useState <
34
+ | {
35
+ label : string ;
36
+ icon ?: string ;
37
+ pages ?: Array < {
38
+ id : string ;
39
+ title : string ;
40
+ href : string ;
41
+ icon ?: string ;
42
+ emoji ?: string ;
43
+ } > ;
44
+ }
45
+ | undefined
46
+ > ( ) ;
19
47
20
48
useEffect ( ( ) => {
21
49
let canceled = false ;
22
50
23
51
( async ( ) => {
24
52
const stream = await streamPageJourneySuggestions ( {
53
+ count : JOURNEY_COUNT ,
25
54
currentPage : {
26
55
id : currentPage . pageId ,
27
56
title : currentPage . title ,
@@ -33,49 +62,84 @@ export function AIPageJourneySuggestions(props: { spaces: { id: string; title: s
33
62
visitedPages,
34
63
} ) ;
35
64
36
- for await ( const journeys of stream ) {
65
+ for await ( const journey of stream ) {
37
66
if ( canceled ) return ;
38
- setJourneys ( journeys ) ;
67
+
68
+ // Find the first empty slot in the journeys array
69
+ setJourneys ( ( prev ) => {
70
+ const newJourneys = [ ...prev ] ;
71
+ const emptyIndex = newJourneys . findIndex ( ( j ) => ! j ?. label ) ;
72
+ if ( emptyIndex >= 0 ) {
73
+ newJourneys [ emptyIndex ] = journey ;
74
+ }
75
+ return newJourneys ;
76
+ } ) ;
39
77
}
40
78
} ) ( ) ;
41
79
42
80
return ( ) => {
43
81
canceled = true ;
44
82
} ;
45
- } , [ currentPage . pageId , currentPage . spaceId , visitedPages , spaces ] ) ;
46
-
47
- const shimmerBlocks = [
48
- '[animation-delay:-.2s]' ,
49
- '[animation-delay:-.4s]' ,
50
- '[animation-delay:-.6s]' ,
51
- '[animation-delay:-.8s]' ,
52
- ] ;
83
+ } , [ currentPage . pageId , currentPage . spaceId , currentPage . title , visitedPages , spaces ] ) ;
53
84
54
85
return (
55
- < div className = "grid w-72 grid-cols-2 gap-2 text-sm" >
56
- { shimmerBlocks . map ( ( block , i ) =>
57
- journeys [ i ] ?. icon ? (
58
- < div
59
- // biome-ignore lint/suspicious/noArrayIndexKey: The index is the only identifier available, since we don't know the content of the block until it's loaded in.
60
- key = { i }
61
- className = "flex animate-fadeIn flex-col items-center justify-center gap-2 rounded border border-tint px-2 py-4 text-center [animation-delay:.2s] [animation-fill-mode:both]"
62
- >
63
- < Icon
64
- icon = { journeys [ i ] . icon as IconName }
65
- className = "size-4 text-tint-subtle"
66
- />
67
- { journeys [ i ] . label }
68
- </ div >
69
- ) : (
70
- < div
71
- // biome-ignore lint/suspicious/noArrayIndexKey: The index is the only identifier available, since we don't know the content of the block until it's loaded in.
86
+ < div >
87
+ < div className = "grid w-72 grid-cols-2 gap-2 text-sm" >
88
+ { journeys . map ( ( journey , i ) => (
89
+ < button
90
+ type = "button"
72
91
key = { i }
92
+ disabled = { journey ?. label === undefined }
73
93
className = { tcls (
74
- 'h-24 animate-pulse rounded-md straight-corners:rounded-none border border-tint-subtle' ,
75
- block
94
+ 'flex flex-col items-center justify-center gap-2 rounded border border-tint-subtle px-2 py-4 text-center transition-all duration-500 *:animate-fadeIn *:delay-200' ,
95
+ journey ?. label === undefined
96
+ ? 'h-24 scale-90 animate-pulse'
97
+ : 'duration-300 hover:border-tint hover:bg-tint-active hover:text-tint-strong' ,
98
+ journey ?. label &&
99
+ journey . label === selected ?. label &&
100
+ 'border-tint bg-tint-active text-tint-strong'
76
101
) }
77
- />
78
- )
102
+ style = { {
103
+ animationDelay : `${ i * - 0.2 } s` ,
104
+ } }
105
+ onClick = { ( ) => setSelected ( journey ) }
106
+ >
107
+ { journey ?. icon ? (
108
+ < Icon
109
+ icon = { journey . icon as IconName }
110
+ className = "size-4 text-tint-subtle"
111
+ />
112
+ ) : null }
113
+ { journey ?. label }
114
+ </ button >
115
+ ) ) }
116
+ </ div >
117
+ { selected && (
118
+ < div className = "mt-6 animate-present text-sm [animation-duration:1000ms]" >
119
+ < h3 className = "font-bold text-base" >
120
+ { selected . icon ? (
121
+ < Icon
122
+ icon = { selected . icon as IconName }
123
+ className = "mr-2 inline size-5 text-tint-subtle"
124
+ />
125
+ ) : null }
126
+ { selected . label }
127
+ </ h3 >
128
+ < ol className = "mt-2 ml-2 flex flex-col gap-2 border-tint-subtle border-l pl-5" >
129
+ { selected . pages ?. map ( ( page , index ) => (
130
+ < li
131
+ key = { selected . label + page . id }
132
+ className = "animate-fadeIn [animation-duration:500ms]"
133
+ style = { { animationDelay : `${ index * 0.1 } s` } }
134
+ >
135
+ < Link href = { page . href } className = "flex gap-2" >
136
+ < Icon icon = { page . icon as IconName } className = "size-4" />
137
+ { page . title }
138
+ </ Link >
139
+ </ li >
140
+ ) ) }
141
+ </ ol >
142
+ </ div >
79
143
) }
80
144
</ div >
81
145
) ;
0 commit comments