Skip to content

Commit 46ead25

Browse files
authored
build: upgrade to React 19 (#624)
* build: upgrade to React 19 Bump react and react-dom to ^19.2.0 (and add @types/react-dom) along with the small type adjustments React 19's stricter definitions require: - useRef<T>() with no initial value now needs an explicit argument, so the setTimeout ref hooks pass undefined. - ReactElement.props is typed as unknown, so the markdown text extractor narrows the element to ReactElement<{ children?: ReactNode }>. - Import the JSX type explicitly in page-actions where the global namespace is no longer ambient. Lockfile regenerated against the public npm registry. Typecheck and production build pass; runtime smoke test green (all routes 200, search and OG generation working, no hydration warnings). * build: upgrade vidstack to v1 for React 19 support @vidstack/react@0.6.15 only declares a React 18 peer; 1.15.6 officially supports React 18 and 19. The v1 API changed, so the Video component is updated accordingly: - MediaOutlet is replaced by MediaProvider. - Base styles import moves to @vidstack/react/player/styles/base.css. - The player ref is typed MediaPlayerInstance and player props use the v1 camelCase form (autoPlay, playsInline); aspectRatio is now a ratio string. - Add the 'use client' directive the hook-based component always needed. Register Video in the MDX component map so docs can use it, and migrate the code_interpreter embed from a raw <video> tag to <Video> as the first adoption. The remaining native <video> embeds are left untouched for now. * docs: migrate remaining native video embeds to the Video component Convert the raw <video> tags in mcp, examples, and web_search to <Video> for a consistent player UX (poster overlay, lazy loading, accessible play button with a descriptive title). The autoplaying example clip uses gifStyle to preserve its ambient looping behavior. The <video> snippet inside the examples.mdx code block is intentionally left as-is since it documents raw tag usage. * fix: resolve a video provider for extensionless sources Vidstack v1 infers the media provider from a string src's file extension. Extensionless URLs, such as GitHub asset links, resolve no provider and the video silently fails to load, unlike the native <video><source> the browser could content-sniff. The Video component now accepts an optional `type` and normalizes its source: an explicit type wins, recognized extensions stay plain strings, and anything else defaults to a video/mp4 source object. The extensionless example embed passes type="video/mp4" explicitly. Verified the example player now reaches canPlay with a resolved video provider. * fix: add a controls-capable autoplay mode to Video gifStyle autoplays muted but loops with no controls and a non-interactive overlay, so the migrated walkthrough clip could no longer be paused, scrubbed, or replayed. That is a regression from the native <video muted autoPlay playsInline controls> and a WCAG 2.2.2 concern for pausing motion. Add an autoPlay prop: muted eager autoplay that keeps the player controls and does not loop or render an overlay. gifStyle stays the ambient, controls-free mode. Switch the example embed to autoPlay so users retain playback controls.
1 parent abc7fcf commit 46ead25

12 files changed

Lines changed: 589 additions & 578 deletions

File tree

‎components/Video.tsx‎

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import 'vidstack/styles/base.css'
1+
'use client'
2+
import '@vidstack/react/player/styles/base.css'
23
import { cn } from '@/lib/utils'
3-
import { MediaPlayer, MediaOutlet, useMediaRemote, useMediaStore } from '@vidstack/react'
4+
import {
5+
MediaPlayer,
6+
MediaProvider,
7+
useMediaRemote,
8+
useMediaStore,
9+
type MediaPlayerInstance,
10+
} from '@vidstack/react'
411
import { Play } from 'lucide-react'
5-
import { useState, useRef } from 'react'
12+
import { useState, useRef, type ComponentProps } from 'react'
613

714
/** Video embed component using vidstack. Supports poster overlays, gif-style autoplay, and lazy loading. */
815
export const Video = ({
@@ -11,33 +18,58 @@ export const Video = ({
1118
aspectRatio,
1219
className,
1320
gifStyle = false,
21+
autoPlay = false,
1422
title,
23+
type,
1524
}: {
1625
src: string
1726
poster?: string
18-
aspectRatio?: number
27+
/** Ratio in `width/height` form, e.g. `"16/9"`. */
28+
aspectRatio?: string
29+
/** Muted, looping, controls-free autoplay (ambient/gif-like). */
1930
gifStyle?: boolean
31+
/** Muted autoplay that keeps player controls so motion can be paused, scrubbed, and replayed. */
32+
autoPlay?: boolean
2033
className?: string
2134
title?: string
35+
/** Explicit MIME type, e.g. `"video/mp4"`. Needed for extensionless URLs. */
36+
type?: string
2237
}) => {
2338
const [panelDismissed, setPanelDismissed] = useState(false)
24-
const mediaPlayerRef = useRef(null)
39+
const mediaPlayerRef = useRef<MediaPlayerInstance>(null)
2540
const remote = useMediaRemote(mediaPlayerRef)
2641
const { duration } = useMediaStore(mediaPlayerRef)
2742
const durationString = duration
2843
? `${Math.floor(duration / 60)}:${Math.floor(duration % 60)} min`
2944
: null
3045

46+
// Vidstack infers the provider from a string src's file extension. Extensionless
47+
// URLs (e.g. GitHub asset links) resolve no provider, so pass an explicit MIME
48+
// type: honor an override, keep recognized extensions as plain strings, and
49+
// default the rest to mp4.
50+
const source = (
51+
type === undefined
52+
? /\.(mp4|webm|ogg|ogv|mov|m4v|m3u8|mpd)(\?|#|$)/i.test(src)
53+
? src
54+
: { src, type: 'video/mp4' }
55+
: { src, type }
56+
) as ComponentProps<typeof MediaPlayer>['src']
57+
58+
// gifStyle and autoPlay both start muted playback eagerly. gifStyle loops with
59+
// no controls (ambient), while autoPlay keeps controls so users can pause,
60+
// scrub, and replay.
61+
const isAutoplay = gifStyle || autoPlay
62+
3163
return (
3264
<MediaPlayer
3365
ref={mediaPlayerRef}
34-
src={src}
35-
controls={!gifStyle && panelDismissed}
36-
autoplay={gifStyle}
37-
muted={gifStyle}
66+
src={source}
67+
controls={!gifStyle && (autoPlay || panelDismissed)}
68+
autoPlay={isAutoplay}
69+
muted={isAutoplay}
3870
loop={gifStyle}
39-
load={gifStyle ? 'eager' : 'custom'}
40-
playsinline={gifStyle}
71+
load={isAutoplay ? 'eager' : 'custom'}
72+
playsInline={isAutoplay}
4173
aspectRatio={aspectRatio}
4274
className={cn(
4375
'my-4 overflow-hidden shadow-lg ring-1 ring-slate-700 bg-cover object-cover',
@@ -47,7 +79,7 @@ export const Video = ({
4779
{gifStyle ? (
4880
// Capture mouse events, they broke scrolling on iOS
4981
<div className="absolute inset-0 z-10" />
50-
) : panelDismissed ? null : (
82+
) : autoPlay || panelDismissed ? null : (
5183
// Overlay with play button and poster image
5284
<div
5385
role="button"
@@ -83,7 +115,7 @@ export const Video = ({
83115
</div>
84116
</div>
85117
)}
86-
<MediaOutlet />
118+
<MediaProvider />
87119
</MediaPlayer>
88120
)
89121
}

‎components/ai/AskAI.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ export function AskAI() {
287287
const scrollRef = useRef<HTMLDivElement>(null)
288288
const triggerRef = useRef<HTMLButtonElement>(null)
289289
const prevOpenRef = useRef(false)
290-
const shareTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
290+
const shareTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
291291
const router = useRouter()
292292
const pathname = usePathname()
293293
const navigatedRef = useRef<Set<string>>(new Set())

‎components/ai/markdown.tsx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { cn } from '@/lib/utils'
99

1010
function CopyButton({ text }: { text: string }) {
1111
const [copied, setCopied] = useState(false)
12-
const timeout = useRef<ReturnType<typeof setTimeout>>()
12+
const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
1313

1414
useEffect(() => {
1515
return () => clearTimeout(timeout.current)
@@ -67,9 +67,13 @@ export function ChatMarkdown({ children }: { children: string }) {
6767
if (typeof node === 'string') {
6868
text += node
6969
} else if (Array.isArray(node)) {
70-
for (const n of node) { extractText(n) }
70+
for (const n of node) {
71+
extractText(n)
72+
}
7173
} else if (node && typeof node === 'object' && 'props' in node) {
72-
extractText((node as React.ReactElement).props.children)
74+
extractText(
75+
(node as React.ReactElement<{ children?: React.ReactNode }>).props.children,
76+
)
7377
}
7478
}
7579
extractText(c)
@@ -130,9 +134,7 @@ export function ChatMarkdown({ children }: { children: string }) {
130134
{c}
131135
</th>
132136
),
133-
td: ({ children: c }) => (
134-
<td className="border border-fd-border px-2 py-1">{c}</td>
135-
),
137+
td: ({ children: c }) => <td className="border border-fd-border px-2 py-1">{c}</td>,
136138
}}
137139
>
138140
{children}

‎components/page-actions.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import { useCallback, useMemo, useRef, useState } from 'react'
2+
import { useCallback, useMemo, useRef, useState, type JSX } from 'react'
33
import { Check, ChevronDown, Copy, ExternalLinkIcon } from 'lucide-react'
44
import { cn } from '@/lib/utils'
55
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -9,7 +9,7 @@ function useCopyButton(
99
onCopy: () => Promise<void> | void,
1010
): [checked: boolean, onClick: () => void] {
1111
const [checked, setChecked] = useState(false)
12-
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
12+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
1313

1414
const onClick = useCallback(() => {
1515
void Promise.resolve(onCopy()).then(() => {

‎content/docs/configuration/librechat_yaml/object_structure/web_search.mdx‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -490,12 +490,7 @@ Here are the steps to set up your own SearXNG instance for use with LibreChat:
490490

491491
Here's a video to guide you through the process from start to finish in about a minute:
492492

493-
<video
494-
muted
495-
controls
496-
>
497-
<source src="/videos/min_searxng_docker_setup_hq.mp4" />
498-
</video>
493+
<Video src="/videos/min_searxng_docker_setup_hq.mp4" title="SearXNG Docker setup walkthrough" />
499494

500495
**Note:** In this example, the instance URL is http://localhost:55011 (port number found under the container name in the top left at the end of the video)
501496

‎content/docs/documentation/examples.mdx‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -759,14 +759,12 @@ The `items` prop on the `<Tabs>` component is an array of strings that represent
759759

760760
<Tabs items={['Example', 'Code']}>
761761
<Tabs.Tab>
762-
<video
763-
muted
762+
<Video
763+
src="https://github.com/danny-avila/LibreChat/assets/32828263/0464f122-eb20-49cd-b201-57db50c57376"
764+
type="video/mp4"
765+
title="Example walkthrough"
764766
autoPlay
765-
playsInline
766-
controls
767-
>
768-
<source src="https://github.com/danny-avila/LibreChat/assets/32828263/0464f122-eb20-49cd-b201-57db50c57376" />
769-
</video>
767+
/>
770768
</Tabs.Tab>
771769

772770
<Tabs.Tab>

‎content/docs/features/code_interpreter.mdx‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ description: Execute code securely and manage files seamlessly with LibreChat's
88

99
LibreChat's Code Interpreter API provides a secure and hassle-free way to execute code and manage files through a simple API interface. Whether you're using it through LibreChat's Agents or integrating it directly into your applications, the API offers a powerful sandbox environment for running code in multiple programming languages.
1010

11-
<video
12-
controls
13-
width="100%"
14-
style={{ marginTop: '2rem', marginBottom: '2rem' }}
15-
>
16-
<source src="https://firebasestorage.googleapis.com/v0/b/superb-reporter-407417.appspot.com/o/Sequence%2001.mp4?alt=media&token=595b0716-12f1-4516-be93-c7004a85b540" type="video/mp4" />
17-
Your browser does not support the video tag.
18-
</video>
11+
<Video
12+
src="https://firebasestorage.googleapis.com/v0/b/superb-reporter-407417.appspot.com/o/Sequence%2001.mp4?alt=media&token=595b0716-12f1-4516-be93-c7004a85b540"
13+
title="Code Interpreter demo"
14+
aspectRatio="16/9"
15+
/>
1916

2017
## Subscription
2118
**Access to this feature requires an [API subscription, get started here](https://code.librechat.ai/pricing).**

‎content/docs/features/mcp.mdx‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -238,30 +238,21 @@ You can initialize or re-initialize MCP servers directly from the interface:
238238

239239
- One-click initialization from the MCP server selection dropdown
240240

241-
<video muted controls>
242-
<source src="/videos/mcp_one_click_init.mp4" />
243-
</video>
241+
<Video src="/videos/mcp_one_click_init.mp4" title="One-click MCP initialization" />
244242

245243
**From MCPConfigDialog:**
246244

247245
- Click the status icon next to an MCP server in the Chat Dropdown to open the MCPConfigDialog
248246
- Configure custom user variables and click the Authenticate/Initialize button depending on the server authentication type
249247

250-
<video
251-
muted
252-
controls
253-
>
254-
<source src="/videos/mcp_config_dialog_auth.mp4" />
255-
</video>
248+
<Video src="/videos/mcp_config_dialog_auth.mp4" title="MCP config dialog authentication" />
256249

257250
**From MCP Settings Panel:**
258251

259252
- Click any server in the server list section of MCP Settings Panel to access configuration and initialization controls
260253
- Configure custom user variables and click the Authenticate/Initialize button depending on the server authentication type
261254

262-
<video muted controls>
263-
<source src="/videos/mcp_settings_init.mp4" />
264-
</video>
255+
<Video src="/videos/mcp_settings_init.mp4" title="MCP settings panel initialization" />
265256

266257
### MCP Settings Panel Visibility
267258

‎lib/mdx-components.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { File as FumadocsFile, Folder, Files } from 'fumadocs-ui/components/file
77
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'
88
import { OptionTable } from '@/components/table'
99
import { Frame } from '@/components/Frame'
10+
import { Video } from '@/components/Video'
1011
import { DocsHub } from '@/components/DocsHub'
1112
import { LocalInstallHub } from '@/components/LocalInstallHub'
1213
import { QuickStartHub } from '@/components/QuickStartHub'
@@ -248,6 +249,7 @@ export const mdxComponents = {
248249
OptionTable,
249250
CompatibilityMatrix,
250251
Frame,
252+
Video,
251253
Carousel,
252254
DocsHub,
253255
QuickStartHub,

‎package.json‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@upstash/ratelimit": "^2.0.8",
3838
"@upstash/redis": "^1.37.0",
3939
"@vercel/og": "^0.6.2",
40-
"@vidstack/react": "^0.6.15",
40+
"@vidstack/react": "^1.15.6",
4141
"ace-builds": "^1.43.5",
4242
"ai": "^6.0.141",
4343
"ajv": "^8.18.0",
@@ -52,9 +52,9 @@
5252
"lucide-react": "^0.378.0",
5353
"next": "^15.5.10",
5454
"next-sitemap": "^4.2.3",
55-
"react": "^18.3.1",
55+
"react": "^19.2.0",
5656
"react-ace": "^11.0.1",
57-
"react-dom": "^18.3.1",
57+
"react-dom": "^19.2.0",
5858
"react-hook-form": "^7.51.4",
5959
"react-hot-toast": "^2.4.1",
6060
"react-markdown": "^10.1.0",
@@ -78,7 +78,8 @@
7878
"@types/js-yaml": "^4.0.9",
7979
"@types/mdx": "^2.0.13",
8080
"@types/node": "18.16.0",
81-
"@types/react": "^18.3.12",
81+
"@types/react": "^19.2.0",
82+
"@types/react-dom": "^19.2.0",
8283
"@types/validator": "^13.15.10",
8384
"autoprefixer": "^10.4.19",
8485
"cross-env": "^7.0.3",

0 commit comments

Comments
 (0)