Skip to content

Commit c716df3

Browse files
committed
Merge branch 'main' into zeno/rnd-7450-mobile-toc-chat-layout
2 parents 8ee84f1 + f478ddc commit c716df3

File tree

23 files changed

+683
-291
lines changed

23 files changed

+683
-291
lines changed

.changeset/chatty-glasses-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Fix missing properties in allOf/oneOf

.changeset/cool-pigs-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Add Input component

.changeset/full-coats-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Fix overflowing section groups

bun.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
"micromark-extension-frontmatter": "^2.0.0",
158158
"micromark-extension-gfm": "^3.0.0",
159159
"motion": "^12.23.24",
160-
"next": "15.4.8",
160+
"next": "15.4.10",
161161
"next-themes": "^0.4.6",
162162
"nuqs": "^2.2.3",
163163
"object-hash": "^3.0.0",
@@ -873,7 +873,7 @@
873873

874874
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
875875

876-
"@next/env": ["@next/env@15.4.8", "", {}, "sha512-LydLa2MDI1NMrOFSkO54mTc8iIHSttj6R6dthITky9ylXV2gCGi0bHQjVCtLGRshdRPjyh2kXbxJukDtBWQZtQ=="],
876+
"@next/env": ["@next/env@15.4.10", "", {}, "sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg=="],
877877

878878
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A=="],
879879

@@ -2517,7 +2517,7 @@
25172517

25182518
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
25192519

2520-
"next": ["next@15.4.8", "", { "dependencies": { "@next/env": "15.4.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-jwOXTz/bo0Pvlf20FSb6VXVeWRssA2vbvq9SdrOPEg9x8E1B27C2rQtvriAn600o9hH61kjrVRexEffv3JybuA=="],
2520+
"next": ["next@15.4.10", "", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="],
25212521

25222522
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
25232523

packages/gitbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"micromark-extension-frontmatter": "^2.0.0",
5151
"micromark-extension-gfm": "^3.0.0",
5252
"motion": "^12.23.24",
53-
"next": "15.4.8",
53+
"next": "15.4.10",
5454
"next-themes": "^0.4.6",
5555
"nuqs": "^2.2.3",
5656
"object-hash": "^3.0.0",

packages/gitbook/src/app/utils.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@
33
import { getDynamicCustomizationSettings } from '@/lib/customization';
44
import type { SiteAPIToken } from '@gitbook/api';
55
import { jwtDecode } from 'jwt-decode';
6-
import { forbidden } from 'next/navigation';
6+
import { forbidden, notFound } from 'next/navigation';
77
import rison from 'rison';
88

99
export type RouteParamMode = 'url-host' | 'url';
@@ -80,14 +80,27 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
8080
* Get the decoded page path from the params.
8181
*/
8282
export function getPagePathFromParams(params: RouteParams) {
83-
const decoded = decodeURIComponent(params.pagePath);
84-
return decoded;
83+
// If decoding the param fails, return a 404 instead of crashing
84+
try {
85+
const decoded = decodeURIComponent(params.pagePath);
86+
return decoded;
87+
} catch (error) {
88+
console.error(
89+
`Returning 404 after failing to decode page path ${params.pagePath}: ${error}`
90+
);
91+
notFound();
92+
}
8593
}
8694

8795
function getSiteURLFromParams(params: RouteLayoutParams) {
88-
const decoded = decodeURIComponent(params.siteURL);
89-
const url = new URL(`https://${decoded}`);
90-
return url;
96+
try {
97+
const decoded = decodeURIComponent(params.siteURL);
98+
const url = new URL(`https://${decoded}`);
99+
return url;
100+
} catch (error) {
101+
console.error(`Returning 404 after failing to decode site URL ${params.siteURL}: ${error}`);
102+
notFound();
103+
}
91104
}
92105

93106
function getModeFromParams(mode: string): RouteParamMode {
@@ -102,6 +115,13 @@ function getModeFromParams(mode: string): RouteParamMode {
102115
* Get the decoded site data from the params.
103116
*/
104117
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
105-
const decoded = decodeURIComponent(params.siteData);
106-
return rison.decode(decoded);
118+
try {
119+
const decoded = decodeURIComponent(params.siteData);
120+
return rison.decode(decoded);
121+
} catch (error) {
122+
console.error(
123+
`Returning 404 after failing to decode site data ${params.siteData}: ${error}`
124+
);
125+
notFound();
126+
}
107127
}

packages/gitbook/src/components/AI/server-actions/api.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ function parseResponse<T>(
128128
parse: (response: AIStreamResponse) => T | undefined | Promise<T | undefined>
129129
): {
130130
stream: EventIterator<T>;
131-
response: Promise<{ responseId: string }>;
131+
response: Promise<{ responseId: string | null }>;
132132
} {
133-
let resolveResponse: (value: { responseId: string }) => void;
134-
const response = new Promise<{ responseId: string }>((resolve) => {
133+
let resolveResponse: (value: { responseId: string | null }) => void;
134+
const response = new Promise<{ responseId: string | null }>((resolve) => {
135135
resolveResponse = resolve;
136136
});
137137

@@ -147,7 +147,7 @@ function parseResponse<T>(
147147

148148
if (event.type === 'response_finish') {
149149
foundResponse = true;
150-
resolveResponse({ responseId: event.responseId });
150+
resolveResponse({ responseId: event.response.id ?? null });
151151
}
152152
}
153153

packages/gitbook/src/components/AI/useAIChat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export function AIChatProvider(props: {
296296
case 'response_finish': {
297297
globalState.setState((state) => ({
298298
...state,
299-
responseId: event.responseId,
299+
responseId: event.response.id ?? null,
300300
// Mark as not loading when the response is finished
301301
// Even if the stream might continue as we receive 'response_followup_suggestion'
302302
loading: false,

packages/gitbook/src/components/AIChat/AIChat.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ export function AIChatBody(props: {
209209
const { chatController, chat, suggestions, greeting } = props;
210210
const { trademark } = useAI().config;
211211

212-
const [input, setInput] = React.useState('');
213212
const language = useLanguage();
214213
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
215214

@@ -279,13 +278,10 @@ export function AIChatBody(props: {
279278
{chat.error ? <AIChatError chatController={chatController} /> : null}
280279

281280
<AIChatInput
282-
value={input}
283-
onChange={setInput}
284281
loading={chat.loading}
285282
disabled={chat.loading || chat.error}
286-
onSubmit={() => {
287-
chatController.postMessage({ message: input });
288-
setInput('');
283+
onSubmit={(value) => {
284+
chatController.postMessage({ message: value });
289285
}}
290286
/>
291287
</div>
Lines changed: 34 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
11
import { t, tString, useLanguage } from '@/intl/client';
2-
import { tcls } from '@/lib/tailwind';
32
import { Icon } from '@gitbook/icons';
43
import { useEffect, useRef } from 'react';
54
import { useHotkeys } from 'react-hotkeys-hook';
65
import { useAIChatState } from '../AI/useAIChat';
7-
import { Button, HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
8-
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
6+
import { HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
7+
import { Input } from '../primitives/Input';
98

109
export function AIChatInput(props: {
11-
value: string;
1210
disabled?: boolean;
1311
/**
1412
* When true, the input is disabled
1513
*/
1614
loading: boolean;
17-
onChange: (value: string) => void;
1815
onSubmit: (value: string) => void;
1916
}) {
20-
const { value, onChange, onSubmit, disabled, loading } = props;
17+
const { onSubmit, disabled, loading } = props;
2118

2219
const language = useLanguage();
2320
const chat = useAIChatState();
2421

2522
const inputRef = useRef<HTMLTextAreaElement>(null);
2623

27-
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
28-
const textarea = event.currentTarget;
29-
onChange(textarea.value);
30-
31-
// Auto-resize
32-
textarea.style.height = 'auto';
33-
textarea.style.height = `${textarea.scrollHeight}px`;
34-
};
35-
3624
useEffect(() => {
3725
if (chat.opened && !disabled && !loading) {
3826
// Add a small delay to ensure the input is rendered before focusing
@@ -57,57 +45,33 @@ export function AIChatInput(props: {
5745
);
5846

5947
return (
60-
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex animate-blur-in-slow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
61-
<textarea
62-
ref={inputRef}
63-
disabled={disabled || loading}
64-
data-loading={loading}
65-
data-testid="ai-chat-input"
66-
className={tcls(
67-
'resize-none',
68-
'focus:outline-hidden',
69-
'focus:ring-0',
70-
'w-full',
71-
'px-3',
72-
'py-3',
73-
'pb-12',
74-
'h-auto',
75-
'bg-transparent',
76-
'peer',
77-
'max-h-64',
78-
'placeholder:text-tint/8',
79-
'transition-colors',
80-
'disabled:bg-tint-subtle',
81-
'delay-300',
82-
'disabled:delay-0',
83-
'disabled:cursor-not-allowed',
84-
'data-[loading=true]:cursor-progress',
85-
'data-[loading=true]:opacity-50'
86-
)}
87-
value={value}
88-
rows={1}
89-
placeholder={tString(language, 'ai_chat_input_placeholder')}
90-
onChange={handleInput}
91-
onKeyDown={(event) => {
92-
if (event.key === 'Escape') {
93-
event.preventDefault();
94-
event.currentTarget.blur();
95-
return;
96-
}
97-
98-
if (event.key === 'Enter' && !event.shiftKey && value.trim()) {
99-
event.preventDefault();
100-
event.currentTarget.style.height = 'auto';
101-
onSubmit(value);
102-
}
103-
}}
104-
/>
105-
{!disabled ? (
106-
<div className="absolute top-2.5 right-3 animate-[fadeIn_0.2s_0.5s_ease-in-out_both] peer-focus:hidden">
107-
<KeyboardShortcut keys={['mod', 'i']} className="bg-tint-base" />
108-
</div>
109-
) : null}
110-
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 px-2 py-2">
48+
<Input
49+
data-testid="ai-chat-input"
50+
name="ai-chat-input"
51+
multiline
52+
resize
53+
sizing="large"
54+
label="Assistant chat input"
55+
placeholder={tString(language, 'ai_chat_input_placeholder')}
56+
onSubmit={(val) => onSubmit(val as string)}
57+
submitButton={{
58+
label: tString(language, 'send'),
59+
}}
60+
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base"
61+
rows={1}
62+
maxLength={2048}
63+
keyboardShortcut={
64+
!disabled && !loading
65+
? {
66+
keys: ['mod', 'i'],
67+
className: 'bg-tint-base group-focus-within/input:hidden',
68+
}
69+
: undefined
70+
}
71+
disabled={disabled || loading}
72+
aria-busy={loading}
73+
ref={inputRef}
74+
trailing={
11175
<HoverCardRoot openDelay={500}>
11276
<HoverCard
11377
className="max-w-xs bg-tint p-2 text-sm text-tint"
@@ -135,7 +99,8 @@ export function AIChatInput(props: {
13599
</div>
136100
</HoverCard>
137101
<HoverCardTrigger>
138-
<div className="flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
102+
{/* Negative margin to compensate for Input's padding, so the badge appears flush with the cursor */}
103+
<div className="-ml-1 flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
139104
<span className="-ml-1 circular-corners:rounded-2xl rounded-corners:rounded-sm bg-tint-11/7 px-1 py-0.5 font-mono font-semibold text-[0.65rem] text-contrast-tint-11 leading-none">
140105
{t(language, 'ai_chat_context_badge')}
141106
</span>{' '}
@@ -146,14 +111,7 @@ export function AIChatInput(props: {
146111
</div>
147112
</HoverCardTrigger>
148113
</HoverCardRoot>
149-
<Button
150-
label={tString(language, 'send')}
151-
size="medium"
152-
className="ml-auto"
153-
disabled={disabled || !value.trim()}
154-
onClick={() => onSubmit(value)}
155-
/>
156-
</div>
157-
</div>
114+
}
115+
/>
158116
);
159117
}

0 commit comments

Comments
 (0)