diff --git a/.changeset/weak-bobcats-trade.md b/.changeset/weak-bobcats-trade.md
new file mode 100644
index 0000000000000000000000000000000000000000..82cf959de376078881cba185b68543e9bb98628a
--- /dev/null
+++ b/.changeset/weak-bobcats-trade.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Display PDF files in source nodes
diff --git a/templates/types/streaming/express/index.ts b/templates/types/streaming/express/index.ts
index 150dbf598c909aa1e70200d458becfb2bbf34f28..5940c09d0b7531cbda98e6e77af7d0a74ba5c4fe 100644
--- a/templates/types/streaming/express/index.ts
+++ b/templates/types/streaming/express/index.ts
@@ -31,6 +31,7 @@ if (isDevelopment) {
   console.warn("Production CORS origin not set, defaulting to no CORS.");
 }
 
+app.use("/api/data", express.static("data"));
 app.use(express.text());
 
 app.get("/", (req: Request, res: Response) => {
diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py
index 1a4e58bebc6cad3e95eca15fefa6d36496a609ac..c053fd6d28f9b12718f9e5562c3e5880ffb9ba98 100644
--- a/templates/types/streaming/fastapi/main.py
+++ b/templates/types/streaming/fastapi/main.py
@@ -11,6 +11,7 @@ from fastapi.responses import RedirectResponse
 from app.api.routers.chat import chat_router
 from app.settings import init_settings
 from app.observability import init_observability
+from fastapi.staticfiles import StaticFiles
 
 
 app = FastAPI()
@@ -20,7 +21,6 @@ init_observability()
 
 environment = os.getenv("ENVIRONMENT", "dev")  # Default to 'development' if not set
 
-
 if environment == "dev":
     logger = logging.getLogger("uvicorn")
     logger.warning("Running in development mode - allowing CORS for all origins")
@@ -38,6 +38,8 @@ if environment == "dev":
         return RedirectResponse(url="/docs")
 
 
+if os.path.exists("data"):
+    app.mount("/api/data", StaticFiles(directory="data"), name="static")
 app.include_router(chat_router, prefix="/api/chat")
 
 
diff --git a/templates/types/streaming/nextjs/app/api/data/[path]/route.ts b/templates/types/streaming/nextjs/app/api/data/[path]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e5fb9271145501634fabd25aeb788ad487d022c
--- /dev/null
+++ b/templates/types/streaming/nextjs/app/api/data/[path]/route.ts
@@ -0,0 +1,38 @@
+import { readFile } from "fs/promises";
+import { NextRequest, NextResponse } from "next/server";
+import path from "path";
+
+/**
+ * This API is to get file data from ./data folder
+ * It receives path slug and response file data like serve static file
+ */
+export async function GET(
+  _request: NextRequest,
+  { params }: { params: { path: string } },
+) {
+  const slug = params.path;
+
+  if (!slug) {
+    return NextResponse.json({ detail: "Missing file slug" }, { status: 400 });
+  }
+
+  if (slug.includes("..") || path.isAbsolute(slug)) {
+    return NextResponse.json({ detail: "Invalid file path" }, { status: 400 });
+  }
+
+  try {
+    const filePath = path.join(process.cwd(), "data", slug);
+    const blob = await readFile(filePath);
+
+    return new NextResponse(blob, {
+      status: 200,
+      statusText: "OK",
+      headers: {
+        "Content-Length": blob.byteLength.toString(),
+      },
+    });
+  } catch (error) {
+    console.error(error);
+    return NextResponse.json({ detail: "File not found" }, { status: 404 });
+  }
+}
diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx
index de8c3edb023fe4a1174ada73572067d73041b2ac..a492eebc50ec094a961e8be949db73e807ca404e 100644
--- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx
+++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-sources.tsx
@@ -1,20 +1,78 @@
-import { ArrowUpRightSquare, Check, Copy } from "lucide-react";
+import { Check, Copy } from "lucide-react";
 import { useMemo } from "react";
 import { Button } from "../button";
 import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
+import { getStaticFileDataUrl } from "../lib/url";
 import { SourceData, SourceNode } from "./index";
 import { useCopyToClipboard } from "./use-copy-to-clipboard";
+import PdfDialog from "./widgets/PdfDialog";
 
-const SCORE_THRESHOLD = 0.5;
+const SCORE_THRESHOLD = 0.3;
+
+function SourceNumberButton({ index }: { index: number }) {
+  return (
+    <div className="text-xs w-5 h-5 rounded-full bg-gray-100 mb-2 flex items-center justify-center hover:text-white hover:bg-primary hover:cursor-pointer">
+      {index + 1}
+    </div>
+  );
+}
+
+enum NODE_TYPE {
+  URL,
+  FILE,
+  UNKNOWN,
+}
+
+type NodeInfo = {
+  id: string;
+  type: NODE_TYPE;
+  path?: string;
+  url?: string;
+};
+
+function getNodeInfo(node: SourceNode): NodeInfo {
+  if (typeof node.metadata["URL"] === "string") {
+    const url = node.metadata["URL"];
+    return {
+      id: node.id,
+      type: NODE_TYPE.URL,
+      path: url,
+      url,
+    };
+  }
+  if (typeof node.metadata["file_path"] === "string") {
+    const fileName = node.metadata["file_name"] as string;
+    return {
+      id: node.id,
+      type: NODE_TYPE.FILE,
+      path: node.metadata["file_path"],
+      url: getStaticFileDataUrl(fileName),
+    };
+  }
+
+  return {
+    id: node.id,
+    type: NODE_TYPE.UNKNOWN,
+  };
+}
 
 export function ChatSources({ data }: { data: SourceData }) {
-  const sources = useMemo(() => {
-    return (
-      data.nodes
-        ?.filter((node) => Object.keys(node.metadata).length > 0)
-        ?.filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
-        .sort((a, b) => (b.score ?? 1) - (a.score ?? 1)) || []
-    );
+  const sources: NodeInfo[] = useMemo(() => {
+    // aggregate nodes by url or file_path (get the highest one by score)
+    const nodesByPath: { [path: string]: NodeInfo } = {};
+
+    data.nodes
+      .filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
+      .sort((a, b) => (b.score ?? 1) - (a.score ?? 1))
+      .forEach((node) => {
+        const nodeInfo = getNodeInfo(node);
+        const key = nodeInfo.path ?? nodeInfo.id; // use id as key for UNKNOWN type
+        if (!nodesByPath[key]) {
+          nodesByPath[key] = nodeInfo;
+        }
+      });
+
+    return Object.values(nodesByPath);
   }, [data.nodes]);
 
   if (sources.length === 0) return null;
@@ -23,55 +81,52 @@ export function ChatSources({ data }: { data: SourceData }) {
     <div className="space-x-2 text-sm">
       <span className="font-semibold">Sources:</span>
       <div className="inline-flex gap-1 items-center">
-        {sources.map((node: SourceNode, index: number) => (
-          <div key={node.id}>
-            <HoverCard>
-              <HoverCardTrigger>
-                <div className="text-xs w-5 h-5 rounded-full bg-gray-100 mb-2 flex items-center justify-center hover:text-white hover:bg-primary hover:cursor-pointer">
-                  {index + 1}
-                </div>
-              </HoverCardTrigger>
-              <HoverCardContent>
-                <NodeInfo node={node} />
-              </HoverCardContent>
-            </HoverCard>
-          </div>
-        ))}
+        {sources.map((nodeInfo: NodeInfo, index: number) => {
+          if (nodeInfo.path?.endsWith(".pdf")) {
+            return (
+              <PdfDialog
+                key={nodeInfo.id}
+                documentId={nodeInfo.id}
+                url={nodeInfo.url!}
+                path={nodeInfo.path}
+                trigger={<SourceNumberButton index={index} />}
+              />
+            );
+          }
+          return (
+            <div key={nodeInfo.id}>
+              <HoverCard>
+                <HoverCardTrigger>
+                  <SourceNumberButton index={index} />
+                </HoverCardTrigger>
+                <HoverCardContent className="w-[320px]">
+                  <NodeInfo nodeInfo={nodeInfo} />
+                </HoverCardContent>
+              </HoverCard>
+            </div>
+          );
+        })}
       </div>
     </div>
   );
 }
 
-function NodeInfo({ node }: { node: SourceNode }) {
+function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) {
   const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
 
-  if (typeof node.metadata["URL"] === "string") {
-    // this is a node generated by the web loader, it contains an external URL
-    // add a link to view this URL
-    return (
-      <a
-        className="space-x-2 flex items-center my-2 hover:text-blue-900"
-        href={node.metadata["URL"]}
-        target="_blank"
-      >
-        <span>{node.metadata["URL"]}</span>
-        <ArrowUpRightSquare className="w-4 h-4" />
-      </a>
-    );
-  }
-
-  if (typeof node.metadata["file_path"] === "string") {
-    // this is a node generated by the file loader, it contains file path
-    // add a button to copy the path to the clipboard
-    const filePath = node.metadata["file_path"];
+  if (nodeInfo.type !== NODE_TYPE.UNKNOWN) {
+    // this is a node generated by the web loader or file loader,
+    // add a link to view its URL and a button to copy the URL to the clipboard
     return (
-      <div className="flex items-center px-2 py-1 justify-between my-2">
-        <span>{filePath}</span>
+      <div className="flex items-center my-2">
+        <a className="hover:text-blue-900" href={nodeInfo.url} target="_blank">
+          <span>{nodeInfo.path}</span>
+        </a>
         <Button
-          onClick={() => copyToClipboard(filePath)}
+          onClick={() => copyToClipboard(nodeInfo.path!)}
           size="icon"
           variant="ghost"
-          className="h-12 w-12"
+          className="h-12 w-12 shrink-0"
         >
           {isCopied ? (
             <Check className="h-4 w-4" />
@@ -84,7 +139,6 @@ function NodeInfo({ node }: { node: SourceNode }) {
   }
 
   // node generated by unknown loader, implement renderer by analyzing logged out metadata
-  console.log("Node metadata", node.metadata);
   return (
     <p>
       Sorry, unknown node type. Please add a new renderer in the NodeInfo
diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..00274546c2132ac9e4a3f76b1f827fc129abb664
--- /dev/null
+++ b/templates/types/streaming/nextjs/app/components/ui/chat/widgets/PdfDialog.tsx
@@ -0,0 +1,56 @@
+import { PDFViewer, PdfFocusProvider } from "@llamaindex/pdf-viewer";
+import { Button } from "../../button";
+import {
+  Drawer,
+  DrawerClose,
+  DrawerContent,
+  DrawerDescription,
+  DrawerHeader,
+  DrawerTitle,
+  DrawerTrigger,
+} from "../../drawer";
+
+export interface PdfDialogProps {
+  documentId: string;
+  path: string;
+  url: string;
+  trigger: React.ReactNode;
+}
+
+export default function PdfDialog(props: PdfDialogProps) {
+  return (
+    <Drawer direction="left">
+      <DrawerTrigger>{props.trigger}</DrawerTrigger>
+      <DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
+        <DrawerHeader className="flex justify-between">
+          <div className="space-y-2">
+            <DrawerTitle>PDF Content</DrawerTitle>
+            <DrawerDescription>
+              File path:{" "}
+              <a
+                className="hover:text-blue-900"
+                href={props.url}
+                target="_blank"
+              >
+                {props.path}
+              </a>
+            </DrawerDescription>
+          </div>
+          <DrawerClose asChild>
+            <Button variant="outline">Close</Button>
+          </DrawerClose>
+        </DrawerHeader>
+        <div className="m-4">
+          <PdfFocusProvider>
+            <PDFViewer
+              file={{
+                id: props.documentId,
+                url: props.url,
+              }}
+            />
+          </PdfFocusProvider>
+        </div>
+      </DrawerContent>
+    </Drawer>
+  );
+}
diff --git a/templates/types/streaming/nextjs/app/components/ui/drawer.tsx b/templates/types/streaming/nextjs/app/components/ui/drawer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bf733c88522a1ad9392fea1ddef177d01b0fafd3
--- /dev/null
+++ b/templates/types/streaming/nextjs/app/components/ui/drawer.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
+
+import { cn } from "./lib/utils";
+
+const Drawer = ({
+  shouldScaleBackground = true,
+  ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
+  <DrawerPrimitive.Root
+    shouldScaleBackground={shouldScaleBackground}
+    {...props}
+  />
+);
+Drawer.displayName = "Drawer";
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+  React.ElementRef<typeof DrawerPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <DrawerPrimitive.Overlay
+    ref={ref}
+    className={cn("fixed inset-0 z-50 bg-black/80", className)}
+    {...props}
+  />
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+  React.ElementRef<typeof DrawerPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
+>(({ className, children, ...props }, ref) => (
+  <DrawerPortal>
+    <DrawerOverlay />
+    <DrawerPrimitive.Content
+      ref={ref}
+      className={cn(
+        "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
+        className,
+      )}
+      {...props}
+    >
+      <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
+      {children}
+    </DrawerPrimitive.Content>
+  </DrawerPortal>
+));
+DrawerContent.displayName = "DrawerContent";
+
+const DrawerHeader = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
+    {...props}
+  />
+);
+DrawerHeader.displayName = "DrawerHeader";
+
+const DrawerFooter = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+    {...props}
+  />
+);
+DrawerFooter.displayName = "DrawerFooter";
+
+const DrawerTitle = React.forwardRef<
+  React.ElementRef<typeof DrawerPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <DrawerPrimitive.Title
+    ref={ref}
+    className={cn(
+      "text-lg font-semibold leading-none tracking-tight",
+      className,
+    )}
+    {...props}
+  />
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+  React.ElementRef<typeof DrawerPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <DrawerPrimitive.Description
+    ref={ref}
+    className={cn("text-sm text-muted-foreground", className)}
+    {...props}
+  />
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+  Drawer,
+  DrawerClose,
+  DrawerContent,
+  DrawerDescription,
+  DrawerFooter,
+  DrawerHeader,
+  DrawerOverlay,
+  DrawerPortal,
+  DrawerTitle,
+  DrawerTrigger,
+};
diff --git a/templates/types/streaming/nextjs/app/components/ui/lib/url.ts b/templates/types/streaming/nextjs/app/components/ui/lib/url.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90236246c83e3f9b5ff18d9e3af98d263de94d4a
--- /dev/null
+++ b/templates/types/streaming/nextjs/app/components/ui/lib/url.ts
@@ -0,0 +1,11 @@
+const STORAGE_FOLDER = "data";
+
+export const getStaticFileDataUrl = (filename: string) => {
+  const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API;
+  const fileUrl = `/api/${STORAGE_FOLDER}/${filename}`;
+  if (isUsingBackend) {
+    const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin;
+    return `${backendOrigin}/${fileUrl}`;
+  }
+  return fileUrl;
+};
diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json
index 182828a4dc436d0f55802a7a243bb8a250022d7b..babe431916ee0db3705d25450cdabdff11654ebf 100644
--- a/templates/types/streaming/nextjs/package.json
+++ b/templates/types/streaming/nextjs/package.json
@@ -32,7 +32,9 @@
     "remark-math": "^5.1.1",
     "rehype-katex": "^7.0.0",
     "supports-color": "^8.1.1",
-    "tailwind-merge": "^2.1.0"
+    "tailwind-merge": "^2.1.0",
+    "vaul": "^0.9.1",
+    "@llamaindex/pdf-viewer": "^1.1.1"
   },
   "devDependencies": {
     "@types/node": "^20.10.3",