diff --git a/.changeset/nice-dolphins-count.md b/.changeset/nice-dolphins-count.md new file mode 100644 index 0000000000000000000000000000000000000000..2a3eae6379eb5f5ebbf9d0a885e58334f5d59723 --- /dev/null +++ b/.changeset/nice-dolphins-count.md @@ -0,0 +1,5 @@ +--- +"@llamaindex/node-parser": patch +--- + +feat: add code splitter and html node parser diff --git a/apps/next/next.config.mjs b/apps/next/next.config.mjs index 3fbc7edaea6f436c04fd7c5930a3e210ca17fb02..3248e75aec380065984782f194c53f0a992f6a42 100644 --- a/apps/next/next.config.mjs +++ b/apps/next/next.config.mjs @@ -1,16 +1,31 @@ import { createMDX } from "fumadocs-mdx/next"; - +import MonacoWebpackPlugin from "monaco-editor-webpack-plugin"; const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, - webpack: (config) => { + transpilePackages: ["monaco-editor"], + webpack: (config, { isServer }) => { + if (Array.isArray(config.target) && config.target.includes("web")) { + config.target = ["web", "es2020"]; + } + config.resolve.alias = { ...config.resolve.alias, sharp$: false, "onnxruntime-node$": false, }; + config.resolve.fallback ??= {}; + config.resolve.fallback.fs = false; + if (!isServer) { + config.plugins.push( + new MonacoWebpackPlugin({ + languages: ["typescript"], + filename: "static/[name].worker.js", + }), + ); + } return config; }, }; diff --git a/apps/next/package.json b/apps/next/package.json index 5bceac14bb4dfb100d047442e0272d6ec3dbc248..7558f2aab8b9f185b7933f10ae03479e9e4d63df 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -14,6 +14,7 @@ "@icons-pack/react-simple-icons": "^10.1.0", "@llamaindex/cloud": "workspace:*", "@llamaindex/core": "workspace:*", + "@llamaindex/node-parser": "workspace:*", "@llamaindex/openai": "workspace:*", "@llamaindex/readers": "workspace:*", "@llamaindex/workflow": "workspace:*", @@ -21,6 +22,8 @@ "@number-flow/react": "^0.3.0", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.3", "@vercel/functions": "^1.5.0", @@ -44,6 +47,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", + "react-monaco-editor": "^0.56.2", "react-text-transition": "^3.1.0", "react-use-measure": "^2.1.1", "rehype-katex": "^7.0.1", @@ -54,7 +58,10 @@ "swr": "^2.2.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "tree-sitter": "^0.22.0", + "tree-sitter-typescript": "^0.23.0", "use-stick-to-bottom": "^1.0.41", + "web-tree-sitter": "^0.24.3", "zod": "^3.23.8" }, "devDependencies": { @@ -66,6 +73,7 @@ "autoprefixer": "^10.4.20", "fast-glob": "^3.3.2", "gray-matter": "^4.0.3", + "monaco-editor-webpack-plugin": "^7.1.0", "postcss": "^8.4.47", "remark": "^15.0.1", "remark-gfm": "^4.0.0", diff --git a/apps/next/public/tree-sitter-typescript.wasm b/apps/next/public/tree-sitter-typescript.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e46182bfce4f22d2c4378ad3f711a591da928c2e Binary files /dev/null and b/apps/next/public/tree-sitter-typescript.wasm differ diff --git a/apps/next/public/tree-sitter.wasm b/apps/next/public/tree-sitter.wasm new file mode 100755 index 0000000000000000000000000000000000000000..e0d76ea3d663d88ff2d5649b3fa20480327b83b8 Binary files /dev/null and b/apps/next/public/tree-sitter.wasm differ diff --git a/apps/next/src/app/(home)/page.tsx b/apps/next/src/app/(home)/page.tsx index 6bc39e61173ea89ed3489f754d865af90c964f59..50b1192e5802aa031e824ecb363f81570ee79bda 100644 --- a/apps/next/src/app/(home)/page.tsx +++ b/apps/next/src/app/(home)/page.tsx @@ -11,7 +11,6 @@ import { NpmInstall } from "@/components/npm-install"; import { TextEffect } from "@/components/text-effect"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; -import { DOCUMENT_URL } from "@/lib/const"; import { SiStackblitz } from "@icons-pack/react-simple-icons"; import { CodeBlock as FumaCodeBlock, @@ -38,7 +37,7 @@ export default function HomePage() { </div> <div className="flex flex-wrap justify-center gap-4"> - <Link href={DOCUMENT_URL}> + <Link href="/docs/llamaindex"> <Button variant="outline">Get Started</Button> </Link> <NpmInstall /> diff --git a/apps/next/src/app/[...any]/page.tsx b/apps/next/src/app/[...any]/page.tsx index 745f2810dc8aad56ee965f63bcd73e861bd322c2..5f3d2f680b241725b9bb0d0e6603d5a8a70f9375 100644 --- a/apps/next/src/app/[...any]/page.tsx +++ b/apps/next/src/app/[...any]/page.tsx @@ -1,4 +1,4 @@ -import { DOCUMENT_URL } from "@/lib/const"; +import { LEGACY_DOCUMENT_URL } from "@/lib/const"; import { redirect } from "next/navigation"; export default async function Page(props: { @@ -7,5 +7,5 @@ export default async function Page(props: { }>; }) { const path = await props.params.then(({ any }) => any.join("/")); - return redirect(new URL(path, DOCUMENT_URL).toString()); + return redirect(new URL(path, LEGACY_DOCUMENT_URL).toString()); } diff --git a/apps/next/src/app/docs/layout.tsx b/apps/next/src/app/docs/layout.tsx index 7d2f6ae7404cc39db3843527e909c0df50d5f921..22fa11b001cb1ba6172e4bbc35e8e5663362aa24 100644 --- a/apps/next/src/app/docs/layout.tsx +++ b/apps/next/src/app/docs/layout.tsx @@ -1,38 +1,54 @@ import { baseOptions } from "@/app/layout.config"; import { AITrigger } from "@/components/ai-chat"; import { buttonVariants } from "@/components/ui/button"; +import { LEGACY_DOCUMENT_URL } from "@/lib/const"; import { source } from "@/lib/source"; import { cn } from "@/lib/utils"; import "fumadocs-twoslash/twoslash.css"; +import { Banner } from "fumadocs-ui/components/banner"; import { DocsLayout } from "fumadocs-ui/layouts/docs"; import { MessageCircle } from "lucide-react"; import type { ReactNode } from "react"; export default function Layout({ children }: { children: ReactNode }) { return ( - <DocsLayout - tree={source.pageTree} - {...baseOptions} - nav={{ - ...baseOptions.nav, - children: ( - <AITrigger - className={cn( - buttonVariants({ - variant: "secondary", - size: "xs", - className: - "md:flex-1 px-2 ms-2 gap-1.5 text-fd-muted-foreground rounded-full", - }), - )} - > - <MessageCircle className="size-3" /> - Ask LlamaCloud - </AITrigger> - ), - }} - > - {children} - </DocsLayout> + <> + <Banner variant="rainbow" id="welcome"> + Welcome to the new LlamaIndex.TS documentation! 🎉 If you are looking + for the old documentation + <a + className="underline text-blue-500 ml-1" + target="_blank" + href={LEGACY_DOCUMENT_URL} + > + check it here + </a> + . + </Banner> + <DocsLayout + tree={source.pageTree} + {...baseOptions} + nav={{ + ...baseOptions.nav, + children: ( + <AITrigger + className={cn( + buttonVariants({ + variant: "secondary", + size: "xs", + className: + "md:flex-1 px-2 ms-2 gap-1.5 text-fd-muted-foreground rounded-full", + }), + )} + > + <MessageCircle className="size-3" /> + Ask LlamaCloud + </AITrigger> + ), + }} + > + {children} + </DocsLayout> + </> ); } diff --git a/apps/next/src/app/layout.config.tsx b/apps/next/src/app/layout.config.tsx index ee86c3bd323b61eb37ed69b0147aeab0d07e4df3..cc2d7c769daedc9a6a29056c8fa1ab3415609f8e 100644 --- a/apps/next/src/app/layout.config.tsx +++ b/apps/next/src/app/layout.config.tsx @@ -1,4 +1,4 @@ -import { DOCUMENT_URL } from "@/lib/const"; +import { Badge } from "@/components/ui/badge"; import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; import Image from "next/image"; @@ -27,8 +27,18 @@ export const baseOptions: BaseLayoutProps = { githubUrl: "https://github.com/run-llama/LlamaIndexTS", links: [ { - text: "Documentation", - url: DOCUMENT_URL, + text: ( + <div className="relative"> + Docs + <Badge + variant="outline" + className="text-blue-500 absolute -top-5 -left-5 bg-fd-background hover:scale-125 transition-transform -rotate-3 hover:-rotate-12" + > + new + </Badge> + </div> + ), + url: "/docs/llamaindex", active: "nested-url", }, ], diff --git a/apps/next/src/app/layout.tsx b/apps/next/src/app/layout.tsx index 0af6d71b5f86a09067ea27f6c7235a0a31d05e79..4a975ed23f6f7980b2c253b0ce2f35a80c7091f5 100644 --- a/apps/next/src/app/layout.tsx +++ b/apps/next/src/app/layout.tsx @@ -1,6 +1,5 @@ import { AIProvider } from "@/actions"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { Banner } from "fumadocs-ui/components/banner"; import { RootProvider } from "fumadocs-ui/provider"; import { Inter } from "next/font/google"; import type { ReactNode } from "react"; @@ -36,14 +35,7 @@ export default function Layout({ children }: { children: ReactNode }) { <body className="flex flex-col min-h-screen"> <TooltipProvider> <AIProvider> - <RootProvider> - <Banner variant="rainbow" id="experimental"> - Welcome to the experimental LlamaIndex.TS documentation site. - Some content are still in progress, you are welcome to help - contribute to the documentation - </Banner> - {children} - </RootProvider> + <RootProvider>{children}</RootProvider> </AIProvider> </TooltipProvider> </body> diff --git a/apps/next/src/components/demo/code-node-parser.tsx b/apps/next/src/components/demo/code-node-parser.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4f66a3ccef50d4f19d8f1b3fd8f57d53b72cf3f8 --- /dev/null +++ b/apps/next/src/components/demo/code-node-parser.tsx @@ -0,0 +1,182 @@ +"use client"; +import { createContextState } from "foxact/context-state"; +import { useIsClient } from "foxact/use-is-client"; +import { useShiki } from "fumadocs-core/utils/use-shiki"; +import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock"; +import { lazy, Suspense, use, useMemo } from "react"; +import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; +import Parser from "web-tree-sitter"; + +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Slider } from "@/components/ui/slider"; +import { CodeSplitter } from "@llamaindex/node-parser/code"; + +let promise: Promise<CodeSplitter>; +if (typeof window !== "undefined") { + promise = Parser.init({ + locateFile(scriptName: string) { + return "/" + scriptName; + }, + }).then(async () => { + const parser = new Parser(); + const Lang = await Parser.Language.load("/tree-sitter-typescript.wasm"); + parser.setLanguage(Lang); + return new CodeSplitter({ + getParser: () => parser, + maxChars: 100, + }); + }); +} + +const [SliderProvider, useSlider, useSetSlider] = createContextState(100); + +const [CodeProvider, useCode, useSetCode] = + createContextState(`interface Person { + name: string; + age: number; +} + +function greet(person: Person): string { + return \`Hello, \${person.name}! You are \${person.age} years old.\`; +} + +const john: Person = { + name: "John Doe", + age: 30 +}; + +console.log(greet(john));`); + +const Editor = lazy(() => import("react-monaco-editor")); + +export const IDE = () => { + const codeSplitter = use(promise); + const code = useCode(); + const setCode = useSetCode(); + const maxChars = useSlider(); + const useSetMaxChars = useSetSlider(); + return ( + <div className="flex flex-col p-4 border-r max-h-96 overflow-scroll"> + <div> + <Label>Max Chars {maxChars}</Label> + <Slider + className="my-4" + min={10} + max={300} + step={10} + value={[maxChars]} + onValueChange={(value) => { + useSetMaxChars(value[0]); + codeSplitter.maxChars = value[0]; + }} + /> + </div> + <Editor + editorWillMount={() => {}} + editorDidMount={() => { + window.MonacoEnvironment!.getWorkerUrl = ( + _moduleId: string, + label: string, + ) => { + if (label === "json") return "/_next/static/json.worker.js"; + if (label === "css") return "/_next/static/css.worker.js"; + if (label === "html") return "/_next/static/html.worker.js"; + if (label === "typescript" || label === "javascript") + return "/_next/static/ts.worker.js"; + return "/_next/static/editor.worker.js"; + }; + }} + editorWillUnmount={() => {}} + options={{ + minimap: { + enabled: false, + }, + }} + theme="vs-dark" + height="100%" + width="100%" + language="typescript" + onChange={setCode} + value={code} + /> + </div> + ); +}; + +const Preview = ({ text }: { text: string }) => { + const rendered = useShiki(text, { + lang: "ts", + components: { + pre: (props) => { + return <Pre {...props}>{props.children}</Pre>; + }, + }, + }); + return <CodeBlock className="py-0 m-2">{rendered}</CodeBlock>; +}; + +function ScrollToBottom() { + const { isAtBottom, scrollToBottom } = useStickToBottomContext(); + + return ( + !isAtBottom && ( + <button + className="absolute i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-0" + onClick={() => scrollToBottom()} + /> + ) + ); +} + +export const NodePreview = () => { + const parser = use(promise); + const code = useCode(); + const maxChars = useSlider(); + const textChunks = useMemo(() => parser.splitText(code), [code, maxChars]); + return ( + <StickToBottom + className="block relative max-h-96 overflow-scroll" + resize="smooth" + initial="smooth" + > + <StickToBottom.Content> + {textChunks.map((chunk, i) => ( + <Preview key={i} text={chunk} /> + ))} + </StickToBottom.Content> + <ScrollToBottom /> + </StickToBottom> + ); +}; + +export const CodeNodeParserDemo = () => { + const isClient = useIsClient(); + if (!isClient) { + return ( + <div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96"> + <Skeleton className="h-96" /> + <Skeleton className="h-96" /> + </div> + ); + } + return ( + <SliderProvider> + <CodeProvider> + <Suspense + fallback={ + <div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96"> + <Skeleton className="h-96" /> + <Skeleton className="h-96" /> + </div> + } + > + <div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96"> + <IDE /> + <NodePreview /> + </div> + </Suspense> + </CodeProvider> + </SliderProvider> + ); +}; diff --git a/apps/next/src/components/demo/workflow-streaming-ui.tsx b/apps/next/src/components/demo/workflow-streaming-ui.tsx index ed273a430e789b9576826e0946fb8c9606f03214..f7b02a2cef7fa5babd8617af3f875f0ddbb79b16 100644 --- a/apps/next/src/components/demo/workflow-streaming-ui.tsx +++ b/apps/next/src/components/demo/workflow-streaming-ui.tsx @@ -84,7 +84,7 @@ function ScrollToBottom() { export function WorkflowStreamingDemo() { const [ui, setUI] = useState<ReactNode[]>([ - <div key={0} className="bg-gray-100"> + <div key={0} className="bg-gray-100 dark:bg-gray-800"> Waiting for workflow to start </div>, ]); @@ -110,28 +110,28 @@ export function WorkflowStreamingDemo() { if (event instanceof ComputeEvent) { setUI((ui) => [ ...ui, - <div key={i++} className="bg-yellow-100"> + <div key={i++} className="bg-yellow-100 dark:bg-yellow-800"> Computing task id: {event.data} </div>, ]); } else if (event instanceof ComputeResultEvent) { setUI((ui) => [ ...ui, - <div key={i++} className="bg-green-100"> + <div key={i++} className="bg-green-100 dark:bg-green-800"> Computed task id: {event.data} </div>, ]); } else if (event instanceof StartEvent) { setUI((ui) => [ ...ui, - <div key={i++} className="bg-blue-100"> + <div key={i++} className="bg-blue-100 dark:bg-blue-800"> Started workflow with total {event.data} </div>, ]); } else if (event instanceof StopEvent) { setUI((ui) => [ ...ui, - <div key={i++} className="bg-red-100"> + <div key={i++} className="bg-red-100 dark:bg-red-800"> Workflow stopped </div>, ]); diff --git a/apps/next/src/components/ui/badge.tsx b/apps/next/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d7ac14df7976ddbd8da468c6670d3077b2a9d70f --- /dev/null +++ b/apps/next/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ); +} + +export { Badge, badgeVariants }; diff --git a/apps/next/src/components/ui/label.tsx b/apps/next/src/components/ui/label.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9bdf1a5044fd94ff1215ccdecbb4059b76f11435 --- /dev/null +++ b/apps/next/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef<typeof LabelPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & + VariantProps<typeof labelVariants> +>(({ className, ...props }, ref) => ( + <LabelPrimitive.Root + ref={ref} + className={cn(labelVariants(), className)} + {...props} + /> +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/apps/next/src/components/ui/slider.tsx b/apps/next/src/components/ui/slider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e43e05055fc7ca79a0abe8b00156d22d53d0a333 --- /dev/null +++ b/apps/next/src/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef<typeof SliderPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> +>(({ className, ...props }, ref) => ( + <SliderPrimitive.Root + ref={ref} + className={cn( + "relative flex w-full touch-none select-none items-center", + className, + )} + {...props} + > + <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"> + <SliderPrimitive.Range className="absolute h-full bg-primary" /> + </SliderPrimitive.Track> + <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" /> + </SliderPrimitive.Root> +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/next/src/content/docs/llamaindex/index.mdx b/apps/next/src/content/docs/llamaindex/index.mdx index 1873bb883962087d05fad13681182ca645131397..f77fa845a03fecce04237acf9d86d558311521bb 100644 --- a/apps/next/src/content/docs/llamaindex/index.mdx +++ b/apps/next/src/content/docs/llamaindex/index.mdx @@ -22,20 +22,10 @@ import { Tab, Tabs } from "fumadocs-ui/components/tabs"; ## What's next? <Cards> - <Card - title="I'm new to RAG" - description="Learn more about RAG (Retrieval-augmented generation) using LlamaIndex.TS." - href="/docs/llamaindex/setup/what-is-rag" - /> <Card title="I want to try LlamaIndex.TS" description="Learn how to use LlamaIndex.TS with different JS runtime and frameworks." - href="/docs/llamaindex/setup" - /> - <Card - title="I want to improve performance when using LlamaIndex.TS" - description="Learn how to improve performance, reduce bundle size, improve accuracy." - href="/docs/llamaindex/performance" + href="/docs/llamaindex/setup/getting-started" /> <Card title="Show me code examples" diff --git a/apps/next/src/content/docs/llamaindex/loading/node-parser.mdx b/apps/next/src/content/docs/llamaindex/loading/node-parser.mdx index 674684f6e42728b27eb226e85abc6e390fa451ea..987d2aec8e183252604047268bdd7f5181c894f1 100644 --- a/apps/next/src/content/docs/llamaindex/loading/node-parser.mdx +++ b/apps/next/src/content/docs/llamaindex/loading/node-parser.mdx @@ -2,6 +2,7 @@ title: Node Parsers / Text Splitters description: Learn how to use Node Parsers and Text Splitters to extract data from documents. --- +import { CodeNodeParserDemo } from '../../../../components/demo/code-node-parser'; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; Node parsers are a simple abstraction that take a list of documents, and chunk them into `Node` objects, such that each node is a specific chunk of the parent document. When a document is broken into nodes, all of it's attributes are inherited to the children nodes (i.e. `metadata`, text and metadata templates, etc.). You can read more about `Node` and `Document` properties [here](./). @@ -10,30 +11,17 @@ Node parsers are a simple abstraction that take a list of documents, and chunk t The `NodeParser` in LlamaIndex is responsible for splitting `Document` objects into more manageable `Node` objects. -<Tabs items={["with reader", "with node:fs"]}> - ```ts twoslash tab="with reader" - import { TextFileReader } from '@llamaindex/readers/text' - import {SentenceSplitter} from '@llamaindex/core/node-parser'; - - const nodeParser = new SentenceSplitter(); - const reader = new TextFileReader(); - const documents = await reader.loadData('path/to/file.txt'); - - const parsedDocuments = nodeParser(documents); - // ^? - - ``` +By default, we will use `Settings.nodeParser` to split the document into nodes. You can also assign a custom `NodeParser` to the `Settings` object. - ```ts twoslash tab="with node:fs" - import fs from 'node:fs/promises'; - import { SentenceSplitter } from '@llamaindex/core/node-parser'; - - const nodeParser = new SentenceSplitter(); +```ts twoslash +import { TextFileReader } from '@llamaindex/readers/text' +import { SentenceSplitter } from '@llamaindex/core/node-parser'; +import { Settings } from 'llamaindex'; - const texts = nodeParser.splitText(await fs.readFile('path/to/file.txt', 'utf-8')); - // ^? - ``` -</Tabs> +const nodeParser = new SentenceSplitter(); +Settings.nodeParser = nodeParser; +// ^? +``` ## TextSplitter @@ -79,4 +67,92 @@ The `MarkdownNodeParser` is a more advanced `NodeParser` that can handle markdow // ^? ``` -</Tabs> \ No newline at end of file +</Tabs> + +## CodeSplitter + +The `CodeSplitter` is a more advanced `NodeParser` that can handle code documents. +It will split the code by AST nodes and then parse the nodes into a `Document` object. + +<Tabs items={["with reader", "with node:fs"]}> + ```ts twoslash tab="with reader" + import { TextFileReader } from '@llamaindex/readers/text' + import { CodeSplitter } from '@llamaindex/node-parser/code' + import Parser from "tree-sitter"; + import TS from "tree-sitter-typescript"; + + const parser = new Parser(); + parser.setLanguage(TS.typescript); + const codeSplitter = new CodeSplitter({ + getParser: () => parser, + }); + const reader = new TextFileReader(); + const documents = await reader.loadData('path/to/file.ts'); + + const parsedDocuments = codeSplitter(documents); + // ^? + ``` + + ```ts twoslash tab="with node:fs" + import fs from 'node:fs/promises'; + import { CodeSplitter } from '@llamaindex/node-parser/code' + import Parser from "tree-sitter"; + import TS from "tree-sitter-typescript"; + + const parser = new Parser(); + parser.setLanguage(TS.typescript); + const codeSplitter = new CodeSplitter({ + getParser: () => parser, + }); + + const parsedDocuments = codeSplitter.splitText(await fs.readFile('path/to/file.ts', 'utf-8')); + // ^? + ``` +</Tabs> + +Try it out ⬇️ + +<CodeNodeParserDemo/> + +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; + +<Accordions> + <Accordion title="Use it in browser"> + You might setup WASM files for `web-tree-sitter` and use it in the browser. + + ```ts + import Parser from 'web-tree-sitter'; + + Parser.init({ + locateFile(scriptName: string) { + return '/' + scriptName + }, + }).then(async () => { + const parser = new Parser(); + const Lang = await Parser.Language.load('/tree-sitter-typescript.wasm'); + parser.setLanguage(Lang); + return new CodeSplitter({ + getParser: () => parser, + maxChars: 100 + }); + }); + ``` + + In this example, you should put `tree-sitter-typescript.wasm` to the `public` folder for Next.js. + + And also update the `next.config.js` to make `@llamaindex/env` work properly. + + ```js + const config = { + webpack: (config) => { + if (Array.isArray(config.target) && config.target.includes('web')) { + config.target = ["web", "es2020"]; + } + return config; + } + } + + export default config; + ``` + </Accordion> +</Accordions> diff --git a/apps/next/src/lib/const.ts b/apps/next/src/lib/const.ts index a6c441bc01c7f51bf1d91f4fac65d964fc4c8a33..96304784d7e7ef78d266ea26c3c9938be342767e 100644 --- a/apps/next/src/lib/const.ts +++ b/apps/next/src/lib/const.ts @@ -1,2 +1,2 @@ // when we are ready, change to /docs/llamaindex -export const DOCUMENT_URL = 'https://legacy.ts.llamaindex.ai/' +export const LEGACY_DOCUMENT_URL = 'https://legacy.ts.llamaindex.ai/' diff --git a/apps/next/types/cache-life.d.ts b/apps/next/types/cache-life.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5970926f8d1a8c131bcf67f93886a38f7c3c964b --- /dev/null +++ b/apps/next/types/cache-life.d.ts @@ -0,0 +1,135 @@ +// Type definitions for Next.js cacheLife configs + +declare module "next/cache" { + export { cacheTag as unstable_cacheTag } from "next/dist/server/use-cache/cache-tag"; + export { + revalidatePath, + revalidateTag, + } from "next/dist/server/web/spec-extension/revalidate"; + export { unstable_cache } from "next/dist/server/web/spec-extension/unstable-cache"; + export { unstable_noStore } from "next/dist/server/web/spec-extension/unstable-no-store"; + + /** + * Cache this `"use cache"` for a timespan defined by the `"default"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 900 seconds (15 minutes) + * expire: never + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 15 minutes, start revalidating new values in the background. + * It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request. + */ + export function unstable_cacheLife(profile: "default"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"seconds"` profile. + * ``` + * stale: 0 seconds + * revalidate: 1 seconds + * expire: 1 seconds + * ``` + * + * This cache may be stale on clients for 0 seconds before checking with the server. + * This cache will expire after 1 seconds. The next request will recompute it. + */ + export function unstable_cacheLife(profile: "seconds"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"minutes"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 60 seconds (1 minute) + * expire: 3600 seconds (1 hour) + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 1 minute, start revalidating new values in the background. + * If this entry has no traffic for 1 hour it will expire. The next request will recompute it. + */ + export function unstable_cacheLife(profile: "minutes"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"hours"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 3600 seconds (1 hour) + * expire: 86400 seconds (1 day) + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 1 hour, start revalidating new values in the background. + * If this entry has no traffic for 1 day it will expire. The next request will recompute it. + */ + export function unstable_cacheLife(profile: "hours"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"days"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 86400 seconds (1 day) + * expire: 604800 seconds (1 week) + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 1 day, start revalidating new values in the background. + * If this entry has no traffic for 1 week it will expire. The next request will recompute it. + */ + export function unstable_cacheLife(profile: "days"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"weeks"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 604800 seconds (1 week) + * expire: 2592000 seconds (30 days) + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 1 week, start revalidating new values in the background. + * If this entry has no traffic for 30 days it will expire. The next request will recompute it. + */ + export function unstable_cacheLife(profile: "weeks"): void; + + /** + * Cache this `"use cache"` for a timespan defined by the `"max"` profile. + * ``` + * stale: 300 seconds (5 minutes) + * revalidate: 2592000 seconds (30 days) + * expire: never + * ``` + * + * This cache may be stale on clients for 5 minutes before checking with the server. + * If the server receives a new request after 30 days, start revalidating new values in the background. + * It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request. + */ + export function unstable_cacheLife(profile: "max"): void; + + /** + * Cache this `"use cache"` using a custom timespan. + * ``` + * stale: ... // seconds + * revalidate: ... // seconds + * expire: ... // seconds + * ``` + * + * This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate` + * + * If a value is left out, the lowest of other cacheLife() calls or the default, is used instead. + */ + export function unstable_cacheLife(profile: { + /** + * This cache may be stale on clients for ... seconds before checking with the server. + */ + stale?: number; + /** + * If the server receives a new request after ... seconds, start revalidating new values in the background. + */ + revalidate?: number; + /** + * If this entry has no traffic for ... seconds it will expire. The next request will recompute it. + */ + expire?: number; + }): void; +} diff --git a/apps/next/types/package.json b/apps/next/types/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3dbc1ca591c0557e35b6004aeba250e6a70b56e3 --- /dev/null +++ b/apps/next/types/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/llamaindex/package.json b/packages/llamaindex/package.json index 83cf7aa9e2ad3bd927f864390e024a128af2bc6d..c21712b0d4fae1c6d91dcdfb4d121af07257e52a 100644 --- a/packages/llamaindex/package.json +++ b/packages/llamaindex/package.json @@ -37,6 +37,7 @@ "@llamaindex/env": "workspace:*", "@llamaindex/groq": "workspace:*", "@llamaindex/huggingface": "workspace:*", + "@llamaindex/node-parser": "workspace:*", "@llamaindex/ollama": "workspace:*", "@llamaindex/openai": "workspace:*", "@llamaindex/portkey-ai": "workspace:*", diff --git a/packages/llamaindex/src/index.edge.ts b/packages/llamaindex/src/index.edge.ts index 944372bb32409ccac4ef1a02f50b14a6fe772d50..888be4b75b0cb202996469b85a7cf78ce46e20dd 100644 --- a/packages/llamaindex/src/index.edge.ts +++ b/packages/llamaindex/src/index.edge.ts @@ -68,7 +68,7 @@ export * from "./indices/index.js"; export * from "./ingestion/index.js"; export { imageToDataUrl } from "./internal/utils.js"; export * from "./llm/index.js"; -export * from "./nodeParsers/index.js"; +export * from "./node-parser.js"; export * from "./objects/index.js"; export * from "./OutputParser.js"; export * from "./postprocessors/index.js"; diff --git a/packages/llamaindex/src/node-parser.ts b/packages/llamaindex/src/node-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3ce3acda0df029ecc4ccd05c4e5b0d64e9598d4 --- /dev/null +++ b/packages/llamaindex/src/node-parser.ts @@ -0,0 +1,3 @@ +export * from "@llamaindex/core/node-parser"; +export * from "@llamaindex/node-parser/code"; +export * from "@llamaindex/node-parser/html"; diff --git a/packages/llamaindex/src/nodeParsers/index.ts b/packages/llamaindex/src/nodeParsers/index.ts deleted file mode 100644 index 63eab73540a1cfdb4ecc702133f395d159133e6d..0000000000000000000000000000000000000000 --- a/packages/llamaindex/src/nodeParsers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@llamaindex/core/node-parser"; diff --git a/packages/llamaindex/tests/MetadataExtractors.test.ts b/packages/llamaindex/tests/MetadataExtractors.test.ts index e35537c1ed0cc4e218016a3ef7d2454751cfe719..5e32e878376465faaf5799524f899ed76a0504bc 100644 --- a/packages/llamaindex/tests/MetadataExtractors.test.ts +++ b/packages/llamaindex/tests/MetadataExtractors.test.ts @@ -10,7 +10,7 @@ import { TitleExtractor, } from "llamaindex/extractors/index"; import { OpenAI } from "llamaindex/llm/openai"; -import { SentenceSplitter } from "llamaindex/nodeParsers/index"; +import { SentenceSplitter } from "llamaindex/node-parser"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { DEFAULT_LLM_TEXT_OUTPUT, diff --git a/packages/llamaindex/tests/ingestion/IngestionCache.test.ts b/packages/llamaindex/tests/ingestion/IngestionCache.test.ts index cfb4dfa1ed10dc84c41af402d8848efc17314fd4..5fd3a3a04f7b1043525411dc7441cd67879041f5 100644 --- a/packages/llamaindex/tests/ingestion/IngestionCache.test.ts +++ b/packages/llamaindex/tests/ingestion/IngestionCache.test.ts @@ -5,7 +5,7 @@ import { IngestionCache, getTransformationHash, } from "llamaindex/ingestion/IngestionCache"; -import { SentenceSplitter } from "llamaindex/nodeParsers/index"; +import { SentenceSplitter } from "llamaindex/node-parser"; import { beforeAll, describe, expect, test } from "vitest"; describe("IngestionCache", () => { diff --git a/packages/node-parser/src/code/index.ts b/packages/node-parser/src/code/index.ts index 7145e06f186c4b64380f8a84e17e8ea1bb004cb5..232213de14954c5fb035ae96be2c2788b7380a19 100644 --- a/packages/node-parser/src/code/index.ts +++ b/packages/node-parser/src/code/index.ts @@ -1,7 +1,12 @@ import { Settings } from "@llamaindex/core/global"; import { TextSplitter } from "@llamaindex/core/node-parser"; -import type Parser from "tree-sitter"; -import type { SyntaxNode } from "tree-sitter"; +import type NodeParser from "tree-sitter"; +import type { SyntaxNode as NodeSyntaxNode } from "tree-sitter"; +import type WebParser from "web-tree-sitter"; +import type { SyntaxNode as WebSyntaxNode } from "web-tree-sitter"; + +type SyntaxNode = NodeSyntaxNode | WebSyntaxNode; +type Parser = NodeParser | WebParser; export type CodeSplitterParam = { getParser: () => Parser; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea5544e8ab891642db7a6671c0d82973c5f64aa9..c70e033978942e0072d54ee2439bbb881c1633ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: '@llamaindex/core': specifier: workspace:* version: link:../../packages/core + '@llamaindex/node-parser': + specifier: workspace:* + version: link:../../packages/node-parser '@llamaindex/openai': specifier: workspace:* version: link:../../packages/providers/openai @@ -157,6 +160,12 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.1 version: 1.3.1(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -226,6 +235,9 @@ importers: react-icons: specifier: ^5.3.0 version: 5.3.0(react@18.3.1) + react-monaco-editor: + specifier: ^0.56.2 + version: 0.56.2(@types/react@18.3.12)(monaco-editor@0.52.0)(react@18.3.1) react-text-transition: specifier: ^3.1.0 version: 3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -256,9 +268,18 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.14) + tree-sitter: + specifier: ^0.22.0 + version: 0.22.0 + tree-sitter-typescript: + specifier: ^0.23.0 + version: 0.23.0(tree-sitter@0.22.0) use-stick-to-bottom: specifier: ^1.0.41 version: 1.0.41(react@18.3.1) + web-tree-sitter: + specifier: ^0.24.3 + version: 0.24.3 zod: specifier: ^3.23.8 version: 3.23.8 @@ -287,6 +308,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + monaco-editor-webpack-plugin: + specifier: ^7.1.0 + version: 7.1.0(monaco-editor@0.52.0)(webpack@5.96.1) postcss: specifier: ^8.4.47 version: 8.4.47 @@ -745,6 +769,9 @@ importers: '@llamaindex/huggingface': specifier: workspace:* version: link:../providers/huggingface + '@llamaindex/node-parser': + specifier: workspace:* + version: link:../node-parser '@llamaindex/ollama': specifier: workspace:* version: link:../providers/ollama @@ -4258,6 +4285,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.0': + resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-navigation-menu@1.2.1': resolution: {integrity: sha512-egDo0yJD2IK8L17gC82vptkvW1jLeni1VuqCyzY727dSJdk5cDjINomouLoNk8RVF7g2aNIfENKWL4UzeU9c8Q==} peerDependencies: @@ -4375,6 +4415,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.2.1': + resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -9736,6 +9789,15 @@ packages: engines: {node: '>=18'} hasBin: true + monaco-editor-webpack-plugin@7.1.0: + resolution: {integrity: sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA==} + peerDependencies: + monaco-editor: '>= 0.31.0' + webpack: ^4.5.0 || 5.x + + monaco-editor@0.52.0: + resolution: {integrity: sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==} + mongodb-connection-string-url@3.0.1: resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} @@ -11129,6 +11191,13 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-monaco-editor@0.56.2: + resolution: {integrity: sha512-Tp5U3QF9h92Cuf0eIhGd8Jyef8tPMlEJC2Dk1GeuR/hj6WoFn8AgjVX/2dv+3l5DvpMUpAECcFarc3eFKTBZ5w==} + peerDependencies: + '@types/react': '>=16 <= 18' + monaco-editor: ^0.52.0 + react: '>=16 <= 18' + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -17430,6 +17499,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-navigation-menu@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -17585,6 +17663,25 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-slider@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -24189,6 +24286,14 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.52.0)(webpack@5.96.1): + dependencies: + loader-utils: 2.0.4 + monaco-editor: 0.52.0 + webpack: 5.96.1 + + monaco-editor@0.52.0: {} + mongodb-connection-string-url@3.0.1: dependencies: '@types/whatwg-url': 11.0.5 @@ -25682,6 +25787,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-monaco-editor@0.56.2(@types/react@18.3.12)(monaco-editor@0.52.0)(react@18.3.1): + dependencies: + '@types/react': 18.3.12 + monaco-editor: 0.52.0 + prop-types: 15.8.1 + react: 18.3.1 + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):