Skip to content

Commit 955fea0

Browse files
committed
Iteration 4
1 parent 9e871bc commit 955fea0

File tree

8 files changed

+121
-140
lines changed

8 files changed

+121
-140
lines changed

packages/gitbook/src/components/Adaptive/AINextPageSuggestions.tsx

Lines changed: 75 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,97 +16,105 @@ export function AINextPageSuggestions() {
1616
const currentPage = usePageContext();
1717
const visitedPages = useVisitedPages((state) => state.pages);
1818

19-
const [pages, setPages] = useState<SuggestedPage[]>(
20-
selectedJourney?.pages ?? Array.from({ length: 5 })
21-
);
19+
const [pages, setPages] = useState<SuggestedPage[]>(selectedJourney?.pages ?? []);
20+
const [suggestedPages, setSuggestedPages] = useState<SuggestedPage[]>([]);
2221

2322
useEffect(() => {
2423
let canceled = false;
2524

2625
if (selectedJourney?.pages && selectedJourney.pages.length > 0) {
2726
setPages(selectedJourney.pages);
27+
} else {
28+
setPages(suggestedPages);
2829
}
2930

30-
(async () => {
31-
const stream = await streamNextPageSuggestions({
32-
currentPage: {
33-
id: currentPage.pageId,
34-
title: currentPage.title,
35-
},
36-
currentSpace: {
37-
id: currentPage.spaceId,
38-
},
39-
visitedPages: visitedPages,
40-
});
31+
if (suggestedPages.length === 0) {
32+
(async () => {
33+
const stream = await streamNextPageSuggestions({
34+
currentPage: {
35+
id: currentPage.pageId,
36+
title: currentPage.title,
37+
},
38+
currentSpace: {
39+
id: currentPage.spaceId,
40+
},
41+
visitedPages: visitedPages,
42+
});
4143

42-
for await (const page of stream) {
43-
if (canceled) return;
44+
for await (const page of stream) {
45+
if (canceled) return;
4446

45-
setPages((prev) => {
46-
const newPages = [...prev];
47-
const emptyIndex = newPages.findIndex((j) => !j?.id);
48-
if (emptyIndex >= 0) {
49-
newPages[emptyIndex] = page;
50-
}
51-
return newPages;
52-
});
53-
}
54-
})();
47+
setPages((prev) => [...prev, page]);
48+
setSuggestedPages((prev) => [...prev, page]);
49+
}
50+
})();
51+
}
5552

5653
return () => {
5754
canceled = true;
5855
};
59-
}, [selectedJourney, currentPage.pageId, currentPage.spaceId, currentPage.title, visitedPages]);
56+
}, [
57+
selectedJourney,
58+
currentPage.pageId,
59+
currentPage.spaceId,
60+
currentPage.title,
61+
visitedPages,
62+
suggestedPages,
63+
]);
6064

6165
return (
62-
<AnimatePresence initial={false}>
63-
{open && (
64-
<motion.div
65-
key="next-page-suggestions"
66-
initial={{ opacity: 0, height: 0 }}
67-
animate={{ opacity: 1, height: 'auto' }}
68-
exit={{ opacity: 0, height: 0 }}
69-
>
70-
<div className="relative mb-2 flex flex-row items-center gap-3">
66+
open && (
67+
<div className="animate-fadeIn">
68+
<motion.div className="mb-2 flex flex-row items-start gap-3">
69+
<AnimatePresence mode="wait">
7170
{selectedJourney?.icon ? (
72-
<Icon
71+
<motion.div
72+
initial={{ opacity: 0, scale: 0.8 }}
73+
animate={{ opacity: 1, scale: 1 }}
74+
exit={{ opacity: 0, scale: 0.8 }}
75+
transition={{ delay: 0.2 }}
7376
key={selectedJourney.icon}
74-
icon={selectedJourney.icon as IconName}
75-
className="absolute left-0 size-6 animate-scaleIn text-tint-subtle [animation-delay:100ms]"
76-
/>
77+
>
78+
<Icon
79+
icon={selectedJourney.icon as IconName}
80+
className="left-0 mt-2 size-6 shrink-0 text-tint-subtle"
81+
/>
82+
</motion.div>
7783
) : null}
78-
<div
79-
className={tcls(
80-
'flex flex-col transition-all',
81-
selectedJourney?.icon ? 'ml-9' : 'delay-0'
82-
)}
83-
>
84-
<div className="flex flex-row items-center gap-2 font-semibold text-tint text-xs uppercase tracking-wide">
85-
Suggested pages
86-
</div>
84+
</AnimatePresence>
85+
<motion.div className={tcls('flex flex-col')} layout="position">
86+
<div className="font-semibold text-tint text-xs uppercase tracking-wide">
87+
Suggested pages
88+
</div>
89+
<AnimatePresence mode="wait">
8790
{selectedJourney?.label ? (
88-
<h5
91+
<motion.h5
92+
initial={{ opacity: 0 }}
93+
animate={{ opacity: 1 }}
94+
exit={{ opacity: 0 }}
8995
key={selectedJourney.label}
90-
className="animate-fadeIn font-semibold text-base"
96+
className="font-semibold text-base"
9197
>
9298
{selectedJourney.label}
93-
</h5>
99+
</motion.h5>
94100
) : null}
95-
</div>
96-
</div>
97-
<div className="-mb-1.5 flex flex-col gap-1">
98-
{pages.map((page, index) =>
99-
page?.id ? (
101+
</AnimatePresence>
102+
</motion.div>
103+
</motion.div>
104+
<div className="-mb-1.5 flex flex-col gap-1">
105+
{Object.assign(Array.from({ length: 5 }), pages).map(
106+
(page: SuggestedPage | undefined, index) =>
107+
page ? (
100108
<Link
101-
key={selectedJourney?.label + page.id}
109+
key={`${selectedJourney?.label}-${page.id}`}
102110
className="-mx-2 flex animate-fadeIn gap-2 rounded px-2.5 py-1 transition-all hover:bg-tint-hover hover:text-tint-strong"
103111
href={page.href}
104112
style={{ animationDelay: `${0.2 + index * 0.05}s` }}
105113
>
106114
{page.icon ? (
107115
<Icon
108116
icon={page.icon as IconName}
109-
className="mt-0.5 size-4 text-tint-subtle"
117+
className="mt-0.5 size-4 shrink-0 text-tint-subtle"
110118
/>
111119
) : null}
112120
{page.emoji ? <Emoji code={page.emoji} /> : null}
@@ -116,13 +124,15 @@ export function AINextPageSuggestions() {
116124
<div
117125
key={index}
118126
className="my-1 h-5 animate-pulse rounded bg-tint-hover"
119-
style={{ animationDelay: `${index * 0.2}s`, width: `${(((index * 17) % 50) + 50)}%` }}
127+
style={{
128+
animationDelay: `${index * 0.2}s`,
129+
width: `${((index * 17) % 50) + 50}%`,
130+
}}
120131
/>
121132
)
122-
)}
123-
</div>
124-
</motion.div>
125-
)}
126-
</AnimatePresence>
133+
)}
134+
</div>
135+
</div>
136+
)
127137
);
128138
}
Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,38 @@
11
'use client';
22
import { tcls } from '@/lib/tailwind';
33
import { Icon, type IconName } from '@gitbook/icons';
4-
import { AnimatePresence, motion } from 'framer-motion';
5-
import { useAdaptiveContext } from './AdaptiveContext';
4+
import { JOURNEY_COUNT, type Journey, useAdaptiveContext } from './AdaptiveContext';
65

76
export function AIPageJourneySuggestions() {
87
const { journeys, selectedJourney, setSelectedJourney, open } = useAdaptiveContext();
98

109
return (
11-
<AnimatePresence initial={false}>
12-
{open && (
13-
<motion.div
14-
key="page-journey-suggestions"
15-
initial={{ opacity: 0, height: 0 }}
16-
animate={{ opacity: 1, height: 'auto' }}
17-
exit={{ opacity: 0, height: 0 }}
18-
>
19-
<div className="mb-2 flex flex-row items-center gap-1 font-semibold text-tint text-xs uppercase tracking-wide">
20-
More to explore
21-
</div>
22-
<div className="grid grid-cols-2 gap-2">
23-
{journeys.map((journey, i) => {
10+
open && (
11+
<div className="animate-fadeIn">
12+
<div className="mb-2 flex flex-row items-center gap-1 font-semibold text-tint text-xs uppercase tracking-wide">
13+
More to explore
14+
</div>
15+
<div className="grid grid-cols-2 gap-2">
16+
{Object.assign(Array.from({ length: JOURNEY_COUNT }), journeys).map(
17+
(journey: Journey | undefined, index) => {
2418
const isSelected =
2519
journey?.label && journey.label === selectedJourney?.label;
26-
const isLoading = journey?.label === undefined;
20+
const isLoading = !journey || journey?.label === undefined;
2721
return (
2822
<button
2923
type="button"
30-
key={i}
24+
key={index}
3125
disabled={journey?.label === undefined}
3226
className={tcls(
3327
'flex flex-col items-center justify-center gap-2 rounded bg-tint px-2 py-4 text-center ring-1 ring-tint-subtle ring-inset transition-all',
3428
isLoading
3529
? 'h-24 scale-90 animate-pulse'
36-
: 'hover:bg-tint-hover hover:text-tint-strong hover:ring-tint',
30+
: 'hover:bg-tint-hover hover:text-tint-strong hover:ring-tint active:scale-95',
3731
isSelected &&
3832
'bg-primary-active text-primary-strong ring-2 ring-primary hover:bg-primary-active hover:ring-primary'
3933
)}
4034
style={{
41-
animationDelay: `${i * 0.2}s`,
35+
animationDelay: `${index * 0.2}s`,
4236
}}
4337
onClick={() =>
4438
setSelectedJourney(isSelected ? undefined : journey)
@@ -51,16 +45,16 @@ export function AIPageJourneySuggestions() {
5145
/>
5246
) : null}
5347
{journey?.label ? (
54-
<span className="animate-fadeIn [animation-delay:400ms]">
48+
<span className="animate-fadeIn leading-tight [animation-delay:400ms]">
5549
{journey.label}
5650
</span>
5751
) : null}
5852
</button>
5953
);
60-
})}
61-
</div>
62-
</motion.div>
63-
)}
64-
</AnimatePresence>
54+
}
55+
)}
56+
</div>
57+
</div>
58+
)
6559
);
6660
}

packages/gitbook/src/components/Adaptive/AdaptiveContext.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type SuggestedPage = {
1313
emoji?: string;
1414
};
1515

16-
type Journey = {
16+
export type Journey = {
1717
label: string;
1818
icon?: string;
1919
pages?: Array<SuggestedPage>;
@@ -30,7 +30,7 @@ type AdaptiveContextType = {
3030

3131
export const AdaptiveContext = React.createContext<AdaptiveContextType | null>(null);
3232

33-
const JOURNEY_COUNT = 4;
33+
export const JOURNEY_COUNT = 4;
3434

3535
/**
3636
* Client side context provider to pass information about the current page.
@@ -39,9 +39,7 @@ export function JourneyContextProvider({
3939
children,
4040
spaces,
4141
}: { children: React.ReactNode; spaces: { id: string; title: string }[] }) {
42-
const [journeys, setJourneys] = React.useState<Journey[]>(
43-
Array.from({ length: JOURNEY_COUNT })
44-
);
42+
const [journeys, setJourneys] = React.useState<Journey[]>([]);
4543
const [selectedJourney, setSelectedJourney] = React.useState<Journey | undefined>(undefined);
4644
const [loading, setLoading] = React.useState(true);
4745
const [open, setOpen] = React.useState(true);
@@ -52,6 +50,8 @@ export function JourneyContextProvider({
5250
useEffect(() => {
5351
let canceled = false;
5452

53+
setJourneys([]);
54+
5555
(async () => {
5656
const stream = await streamPageJourneySuggestions({
5757
count: JOURNEY_COUNT,
@@ -69,14 +69,7 @@ export function JourneyContextProvider({
6969
for await (const journey of stream) {
7070
if (canceled) return;
7171

72-
setJourneys((prev) => {
73-
const newJourneys = [...prev];
74-
const emptyIndex = newJourneys.findIndex((j) => !j?.label);
75-
if (emptyIndex >= 0) {
76-
newJourneys[emptyIndex] = journey;
77-
}
78-
return newJourneys;
79-
});
72+
setJourneys((prev) => [...prev, journey]);
8073
}
8174

8275
setLoading(false);

packages/gitbook/src/components/Adaptive/AdaptivePane.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function AdaptivePane() {
1212
<div
1313
className={tcls(
1414
'flex flex-col gap-4 rounded-md straight-corners:rounded-none bg-tint-subtle ring-1 ring-tint-subtle ring-inset transition-all duration-300',
15-
open ? 'w-72 px-4 py-4' : 'w-56 p-3'
15+
open ? 'w-72 px-4 py-4' : 'w-56 px-4 py-3'
1616
)}
1717
>
1818
<AdaptivePaneHeader />

packages/gitbook/src/components/Adaptive/AdaptivePaneHeader.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ export function AdaptivePaneHeader() {
99
const { loading, open, setOpen } = useAdaptiveContext();
1010

1111
return (
12-
<div
13-
className={tcls(
14-
'flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none transition-all duration-500',
15-
open ? '' : ''
16-
)}
17-
>
12+
<div className="flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none transition-all duration-500">
1813
<div className="flex grow flex-col">
1914
<h4 className="flex items-center gap-1.5 font-semibold ">
2015
<Loading className="size-4 text-tint-subtle" busy={loading} />

0 commit comments

Comments
 (0)