├── .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 | |
172 | ),
173 | td: ({ className, ...props }) => (
174 | |
181 | ),
182 | tr: ({ className, ...props }) => (
183 | 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 |
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 |
--------------------------------------------------------------------------------