Skip to content

Commit 80cb951

Browse files
committed
Second iteration
1 parent e054805 commit 80cb951

File tree

5 files changed

+276
-137
lines changed

5 files changed

+276
-137
lines changed

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

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,56 @@
11
'use client';
2-
32
import { tcls } from '@/lib/tailwind';
43
import { Icon, type IconName } from '@gitbook/icons';
4+
import Link from 'next/link';
55
import { useEffect } from 'react';
66
import { useState } from 'react';
77
import { useVisitedPages } from '../Insights';
88
import { usePageContext } from '../PageContext';
99
import { streamPageJourneySuggestions } from './server-actions';
1010

11+
const JOURNEY_COUNT = 4;
12+
1113
export function AIPageJourneySuggestions(props: { spaces: { id: string; title: string }[] }) {
1214
const { spaces } = props;
1315

1416
const currentPage = usePageContext();
1517

1618
// const language = useLanguage();
1719
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+
>();
1947

2048
useEffect(() => {
2149
let canceled = false;
2250

2351
(async () => {
2452
const stream = await streamPageJourneySuggestions({
53+
count: JOURNEY_COUNT,
2554
currentPage: {
2655
id: currentPage.pageId,
2756
title: currentPage.title,
@@ -33,49 +62,84 @@ export function AIPageJourneySuggestions(props: { spaces: { id: string; title: s
3362
visitedPages,
3463
});
3564

36-
for await (const journeys of stream) {
65+
for await (const journey of stream) {
3766
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+
});
3977
}
4078
})();
4179

4280
return () => {
4381
canceled = true;
4482
};
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]);
5384

5485
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"
7291
key={i}
92+
disabled={journey?.label === undefined}
7393
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'
76101
)}
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>
79143
)}
80144
</div>
81145
);

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import type { SiteStructure } from '@gitbook/api';
2+
import { Icon } from '@gitbook/icons';
23
import type { GitBookSiteContext } from '@v2/lib/context';
34
import { AIPageJourneySuggestions } from './AIPageJourneySuggestions';
45

56
export function AdaptivePane(props: { context: GitBookSiteContext }) {
67
const { context } = props;
78

89
return (
9-
<div>
10-
<AIPageJourneySuggestions spaces={getSpaces(context.structure)} />
11-
</div>
10+
<>
11+
<div>
12+
<div className="mb-2 flex flex-row items-center gap-2 font-semibold text-xs uppercase tracking-wide">
13+
<Icon icon="map" className="size-3" />
14+
More to explore
15+
</div>
16+
<AIPageJourneySuggestions spaces={getSpaces(context.structure)} />
17+
</div>
18+
</>
1219
);
1320
}
1421

0 commit comments

Comments
 (0)