├── .env.example ├── .gitignore ├── README.md ├── app ├── api │ └── [..._path] │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── MyAssistant.tsx ├── assistant-ui │ ├── markdown-text.tsx │ ├── thread.tsx │ └── tooltip-icon-button.tsx └── ui │ ├── button.tsx │ └── tooltip.tsx ├── eslint.config.mjs ├── lib ├── chatApi.ts └── utils.ts ├── next.config.ts ├── package.json ├── postcss.config.mjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | LANGCHAIN_API_KEY=your_langchain_api_key 2 | LANGGRAPH_API_URL=your_langgraph_api_url 3 | NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=your_assistant_id_or_graph_id -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the [assistant-ui](https://github.com/Yonom/assistant-ui) starter project for langgraph. 2 | 3 | ## Getting Started 4 | 5 | First, add your langgraph API url and assistant id to `.env.local` file: 6 | 7 | ``` 8 | LANGCHAIN_API_KEY=your_langchain_api_key 9 | LANGGRAPH_API_URL=your_langgraph_api_url 10 | NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=your_assistant_id_or_graph_id 11 | ``` 12 | 13 | Then, run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | # or 18 | yarn dev 19 | # or 20 | pnpm dev 21 | # or 22 | bun dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 28 | -------------------------------------------------------------------------------- /app/api/[..._path]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export const runtime = "edge"; 4 | 5 | function getCorsHeaders() { 6 | return { 7 | "Access-Control-Allow-Origin": "*", 8 | "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", 9 | "Access-Control-Allow-Headers": "*", 10 | }; 11 | } 12 | 13 | async function handleRequest(req: NextRequest, method: string) { 14 | try { 15 | const path = req.nextUrl.pathname.replace(/^\/?api\//, ""); 16 | const url = new URL(req.url); 17 | const searchParams = new URLSearchParams(url.search); 18 | searchParams.delete("_path"); 19 | searchParams.delete("nxtP_path"); 20 | const queryString = searchParams.toString() 21 | ? `?${searchParams.toString()}` 22 | : ""; 23 | 24 | const options: RequestInit = { 25 | method, 26 | headers: { 27 | "x-api-key": process.env["LANGCHAIN_API_KEY"] || "", 28 | }, 29 | }; 30 | 31 | if (["POST", "PUT", "PATCH"].includes(method)) { 32 | options.body = await req.text(); 33 | } 34 | 35 | console.log({ url, path, queryString, options }); 36 | 37 | const res = await fetch( 38 | `${process.env["LANGGRAPH_API_URL"]}/${path}${queryString}`, 39 | options 40 | ); 41 | 42 | return new NextResponse(res.body, { 43 | status: res.status, 44 | statusText: res.statusText, 45 | headers: { 46 | ...res.headers, 47 | ...getCorsHeaders(), 48 | }, 49 | }); 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | } catch (e: any) { 52 | return NextResponse.json({ error: e.message }, { status: e.status ?? 500 }); 53 | } 54 | } 55 | 56 | export const GET = (req: NextRequest) => handleRequest(req, "GET"); 57 | export const POST = (req: NextRequest) => handleRequest(req, "POST"); 58 | export const PUT = (req: NextRequest) => handleRequest(req, "PUT"); 59 | export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH"); 60 | export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE"); 61 | 62 | // Add a new OPTIONS handler 63 | export const OPTIONS = () => { 64 | return new NextResponse(null, { 65 | status: 204, 66 | headers: { 67 | ...getCorsHeaders(), 68 | }, 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assistant-ui/assistant-ui-starter-langgraph/40637ec59612bd4dd1e83bbf35407a13af44d1f1/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.141 0.005 285.823); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.141 0.005 285.823); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.141 0.005 285.823); 54 | --primary: oklch(0.21 0.006 285.885); 55 | --primary-foreground: oklch(0.985 0 0); 56 | --secondary: oklch(0.967 0.001 286.375); 57 | --secondary-foreground: oklch(0.21 0.006 285.885); 58 | --muted: oklch(0.967 0.001 286.375); 59 | --muted-foreground: oklch(0.552 0.016 285.938); 60 | --accent: oklch(0.967 0.001 286.375); 61 | --accent-foreground: oklch(0.21 0.006 285.885); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.92 0.004 286.32); 64 | --input: oklch(0.92 0.004 286.32); 65 | --ring: oklch(0.705 0.015 286.067); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0 0); 72 | --sidebar-foreground: oklch(0.141 0.005 285.823); 73 | --sidebar-primary: oklch(0.21 0.006 285.885); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.967 0.001 286.375); 76 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 77 | --sidebar-border: oklch(0.92 0.004 286.32); 78 | --sidebar-ring: oklch(0.705 0.015 286.067); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.141 0.005 285.823); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.21 0.006 285.885); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.21 0.006 285.885); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.92 0.004 286.32); 89 | --primary-foreground: oklch(0.21 0.006 285.885); 90 | --secondary: oklch(0.274 0.006 286.033); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.274 0.006 286.033); 93 | --muted-foreground: oklch(0.705 0.015 286.067); 94 | --accent: oklch(0.274 0.006 286.033); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.552 0.016 285.938); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.21 0.006 285.885); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.274 0.006 286.033); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.552 0.016 285.938); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "assistant-ui App", 17 | description: "Generated by create-assistant-ui", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { MyAssistant } from "@/components/MyAssistant"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/MyAssistant.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef } from "react"; 4 | import { AssistantRuntimeProvider } from "@assistant-ui/react"; 5 | import { useLangGraphRuntime } from "@assistant-ui/react-langgraph"; 6 | 7 | import { createThread, getThreadState, sendMessage } from "@/lib/chatApi"; 8 | import { Thread } from "@/components/assistant-ui/thread"; 9 | 10 | export function MyAssistant() { 11 | const threadIdRef = useRef(undefined); 12 | const runtime = useLangGraphRuntime({ 13 | threadId: threadIdRef.current, 14 | stream: async (messages, { command }) => { 15 | if (!threadIdRef.current) { 16 | const { thread_id } = await createThread(); 17 | threadIdRef.current = thread_id; 18 | } 19 | const threadId = threadIdRef.current; 20 | return sendMessage({ 21 | threadId, 22 | messages, 23 | command, 24 | }); 25 | }, 26 | onSwitchToNewThread: async () => { 27 | const { thread_id } = await createThread(); 28 | threadIdRef.current = thread_id; 29 | }, 30 | onSwitchToThread: async (threadId) => { 31 | const state = await getThreadState(threadId); 32 | threadIdRef.current = threadId; 33 | return { messages: state.values.messages }; 34 | }, 35 | }); 36 | 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/assistant-ui/markdown-text.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "@assistant-ui/react-markdown/styles/dot.css"; 4 | 5 | import { 6 | CodeHeaderProps, 7 | MarkdownTextPrimitive, 8 | unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, 9 | useIsMarkdownCodeBlock, 10 | } from "@assistant-ui/react-markdown"; 11 | import remarkGfm from "remark-gfm"; 12 | import { FC, memo, useState } from "react"; 13 | import { CheckIcon, CopyIcon } from "lucide-react"; 14 | 15 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; 16 | import { cn } from "@/lib/utils"; 17 | 18 | const MarkdownTextImpl = () => { 19 | return ( 20 | 25 | ); 26 | }; 27 | 28 | export const MarkdownText = memo(MarkdownTextImpl); 29 | 30 | const CodeHeader: FC = ({ language, code }) => { 31 | const { isCopied, copyToClipboard } = useCopyToClipboard(); 32 | const onCopy = () => { 33 | if (!code || isCopied) return; 34 | copyToClipboard(code); 35 | }; 36 | 37 | return ( 38 |
39 | {language} 40 | 41 | {!isCopied && } 42 | {isCopied && } 43 | 44 |
45 | ); 46 | }; 47 | 48 | const useCopyToClipboard = ({ 49 | copiedDuration = 3000, 50 | }: { 51 | copiedDuration?: number; 52 | } = {}) => { 53 | const [isCopied, setIsCopied] = useState(false); 54 | 55 | const copyToClipboard = (value: string) => { 56 | if (!value) return; 57 | 58 | navigator.clipboard.writeText(value).then(() => { 59 | setIsCopied(true); 60 | setTimeout(() => setIsCopied(false), copiedDuration); 61 | }); 62 | }; 63 | 64 | return { isCopied, copyToClipboard }; 65 | }; 66 | 67 | const defaultComponents = memoizeMarkdownComponents({ 68 | h1: ({ className, ...props }) => ( 69 |

76 | ), 77 | h2: ({ className, ...props }) => ( 78 |

85 | ), 86 | h3: ({ className, ...props }) => ( 87 |

94 | ), 95 | h4: ({ className, ...props }) => ( 96 |

103 | ), 104 | h5: ({ className, ...props }) => ( 105 |

112 | ), 113 | h6: ({ className, ...props }) => ( 114 |
118 | ), 119 | p: ({ className, ...props }) => ( 120 |

124 | ), 125 | a: ({ className, ...props }) => ( 126 | 133 | ), 134 | blockquote: ({ className, ...props }) => ( 135 |

139 | ), 140 | ul: ({ className, ...props }) => ( 141 |
    li]:mt-2", className)} 143 | {...props} 144 | /> 145 | ), 146 | ol: ({ className, ...props }) => ( 147 |
      li]:mt-2", className)} 149 | {...props} 150 | /> 151 | ), 152 | hr: ({ className, ...props }) => ( 153 |
      154 | ), 155 | table: ({ className, ...props }) => ( 156 | 163 | ), 164 | th: ({ className, ...props }) => ( 165 | td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", 186 | className 187 | )} 188 | {...props} 189 | /> 190 | ), 191 | sup: ({ className, ...props }) => ( 192 | a]:text-xs [&>a]:no-underline", className)} 194 | {...props} 195 | /> 196 | ), 197 | pre: ({ className, ...props }) => ( 198 |
      205 |   ),
      206 |   code: function Code({ className, ...props }) {
      207 |     const isCodeBlock = useIsMarkdownCodeBlock();
      208 |     return (
      209 |       
      216 |     );
      217 |   },
      218 |   CodeHeader,
      219 | });
      220 | 
      
      
      --------------------------------------------------------------------------------
      /components/assistant-ui/thread.tsx:
      --------------------------------------------------------------------------------
        1 | import {
        2 |   ActionBarPrimitive,
        3 |   BranchPickerPrimitive,
        4 |   ComposerPrimitive,
        5 |   MessagePrimitive,
        6 |   ThreadPrimitive,
        7 | } from "@assistant-ui/react";
        8 | import type { FC } from "react";
        9 | import {
       10 |   ArrowDownIcon,
       11 |   CheckIcon,
       12 |   ChevronLeftIcon,
       13 |   ChevronRightIcon,
       14 |   CopyIcon,
       15 |   PencilIcon,
       16 |   RefreshCwIcon,
       17 |   SendHorizontalIcon,
       18 | } from "lucide-react";
       19 | import { cn } from "@/lib/utils";
       20 | 
       21 | import { Button } from "@/components/ui/button";
       22 | import { MarkdownText } from "@/components/assistant-ui/markdown-text";
       23 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
       24 | 
       25 | export const Thread: FC = () => {
       26 |   return (
       27 |     
       33 |       
       34 |         
       35 | 
       36 |         
       43 | 
       44 |         
       45 |           
      46 | 47 | 48 |
      49 | 50 | 51 |
      52 | 53 | 54 | ); 55 | }; 56 | 57 | const ThreadScrollToBottom: FC = () => { 58 | return ( 59 | 60 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | const ThreadWelcome: FC = () => { 72 | return ( 73 | 74 |
      75 |
      76 |

      How can I help you today?

      77 |
      78 | 79 |
      80 |
      81 | ); 82 | }; 83 | 84 | const ThreadWelcomeSuggestions: FC = () => { 85 | return ( 86 |
      87 | 93 | 94 | What is the weather in Tokyo? 95 | 96 | 97 | 103 | 104 | What is assistant-ui? 105 | 106 | 107 |
      108 | ); 109 | }; 110 | 111 | const Composer: FC = () => { 112 | return ( 113 | 114 | 120 | 121 | 122 | ); 123 | }; 124 | 125 | const ComposerAction: FC = () => { 126 | return ( 127 | <> 128 | 129 | 130 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 146 | 147 | 148 | 149 | 150 | 151 | ); 152 | }; 153 | 154 | const UserMessage: FC = () => { 155 | return ( 156 | 157 | 158 | 159 |
      160 | 161 |
      162 | 163 | 164 |
      165 | ); 166 | }; 167 | 168 | const UserActionBar: FC = () => { 169 | return ( 170 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ); 182 | }; 183 | 184 | const EditComposer: FC = () => { 185 | return ( 186 | 187 | 188 | 189 |
      190 | 191 | 192 | 193 | 194 | 195 | 196 |
      197 |
      198 | ); 199 | }; 200 | 201 | const AssistantMessage: FC = () => { 202 | return ( 203 | 204 |
      205 | 206 |
      207 | 208 | 209 | 210 | 211 |
      212 | ); 213 | }; 214 | 215 | const AssistantActionBar: FC = () => { 216 | return ( 217 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | ); 240 | }; 241 | 242 | const BranchPicker: FC = ({ 243 | className, 244 | ...rest 245 | }) => { 246 | return ( 247 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | / 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ); 270 | }; 271 | 272 | const CircleStopIcon = () => { 273 | return ( 274 | 281 | 282 | 283 | ); 284 | }; 285 | -------------------------------------------------------------------------------- /components/assistant-ui/tooltip-icon-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ComponentPropsWithoutRef, forwardRef } from "react"; 4 | 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from "@/components/ui/tooltip"; 11 | import { Button } from "@/components/ui/button"; 12 | import { cn } from "@/lib/utils"; 13 | 14 | export type TooltipIconButtonProps = ComponentPropsWithoutRef & { 15 | tooltip: string; 16 | side?: "top" | "bottom" | "left" | "right"; 17 | }; 18 | 19 | export const TooltipIconButton = forwardRef< 20 | HTMLButtonElement, 21 | TooltipIconButtonProps 22 | >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { 23 | return ( 24 | 25 | 26 | 27 | 37 | 38 | {tooltip} 39 | 40 | 41 | ); 42 | }); 43 | 44 | TooltipIconButton.displayName = "TooltipIconButton"; 45 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 62 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /lib/chatApi.ts: -------------------------------------------------------------------------------- 1 | import { Client, ThreadState } from "@langchain/langgraph-sdk"; 2 | import { 3 | LangChainMessage, 4 | LangGraphCommand, 5 | } from "@assistant-ui/react-langgraph"; 6 | 7 | const createClient = () => { 8 | const apiUrl = 9 | process.env["NEXT_PUBLIC_LANGGRAPH_API_URL"] || 10 | new URL("/api", window.location.href).href; 11 | return new Client({ 12 | apiUrl, 13 | }); 14 | }; 15 | 16 | export const createThread = async () => { 17 | const client = createClient(); 18 | return client.threads.create(); 19 | }; 20 | 21 | export const getThreadState = async ( 22 | threadId: string 23 | ): Promise> => { 24 | const client = createClient(); 25 | return client.threads.getState(threadId); 26 | }; 27 | 28 | export const sendMessage = async (params: { 29 | threadId: string; 30 | messages?: LangChainMessage[]; 31 | command?: LangGraphCommand | undefined; 32 | }) => { 33 | const client = createClient(); 34 | return client.runs.stream( 35 | params.threadId, 36 | process.env["NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID"]!, 37 | { 38 | input: params.messages?.length 39 | ? { 40 | messages: params.messages, 41 | } 42 | : null, 43 | command: params.command, 44 | streamMode: ["messages", "updates"], 45 | } 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assistant-ui-starter-langgraph", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ai-sdk/openai": "^1.3.10", 13 | "@assistant-ui/react": "^0.9.1", 14 | "@assistant-ui/react-ai-sdk": "^0.9.1", 15 | "@assistant-ui/react-markdown": "^0.9.1", 16 | "@radix-ui/react-slot": "^1.2.0", 17 | "@radix-ui/react-tooltip": "^1.2.0", 18 | "ai": "^4.3.5", 19 | "class-variance-authority": "^0.7.1", 20 | "clsx": "^2.1.1", 21 | "lucide-react": "^0.487.0", 22 | "next": "15.3.0", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "remark-gfm": "^4.0.1", 26 | "tailwind-merge": "^3.2.0", 27 | "tw-animate-css": "^1.2.5" 28 | }, 29 | "devDependencies": { 30 | "@eslint/eslintrc": "^3", 31 | "@tailwindcss/postcss": "^4", 32 | "@types/node": "^20", 33 | "@types/react": "^19", 34 | "@types/react-dom": "^19", 35 | "eslint": "^9", 36 | "eslint-config-next": "15.3.0", 37 | "tailwindcss": "^4", 38 | "typescript": "^5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------
      172 | ), 173 | td: ({ className, ...props }) => ( 174 | 181 | ), 182 | tr: ({ className, ...props }) => ( 183 |