├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── ai ├── graph.tsx ├── message.tsx └── tools │ ├── firecrawl.tsx │ ├── github_repo.tsx │ ├── index.ts │ ├── invoice.tsx │ └── weather.tsx ├── app ├── agent.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── shared.tsx ├── components.json ├── components ├── prebuilt │ ├── chat.tsx │ ├── github.tsx │ ├── invoice.tsx │ ├── message.tsx │ ├── weather.tsx │ └── web.tsx └── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── lib └── utils.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── gen_ui_diagram.png ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json ├── utils ├── client.tsx └── server.tsx └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # ------------------LangSmith tracing------------------ 2 | LANGCHAIN_API_KEY= 3 | LANGCHAIN_CALLBACKS_BACKGROUND=true 4 | LANGCHAIN_TRACING_V2=true 5 | # ----------------------------------------------------- 6 | 7 | GITHUB_TOKEN= 8 | OPENAI_API_KEY= 9 | GEOCODE_API_KEY= 10 | FIRECRAWL_API_KEY= 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generative UI with LangChain.js 🦜🔗 2 | 3 | ![Generative UI with LangChain.js](./public/gen_ui_diagram.png) 4 | 5 | ## Overview 6 | 7 | This application aims to provide a template for building generative UI applications with LangChain.js. 8 | It comes pre-built with a few UI features which you can use to play about with gen ui. The UI components are built using [Shadcn](https://ui.shadcn.com/). 9 | 10 | ## Getting Started 11 | 12 | ### Installation 13 | 14 | First, clone the repository and install dependencies: 15 | 16 | ```bash 17 | git clone https://github.com/bracesproul/gen-ui.git 18 | 19 | cd gen-ui 20 | 21 | yarn install 22 | ``` 23 | 24 | Next, if you plan on using the existing pre-built UI components, you'll need to set a few environment variables: 25 | 26 | Copy the [`.env.example`](./.env.example) file to `.env`: 27 | 28 | The `OPENAI_API_KEY` is required. LangSmith keys are optional, but highly recommended if you plan on developing this application further. 29 | 30 | Get your OpenAI API key from the [OpenAI dashboard](https://platform.openai.com/login?launch). 31 | 32 | [Sign up/in to LangSmith](https://smith.langchain.com/) and get your API key. 33 | 34 | Create a new [GitHub PAT (Personal Access Token)](https://github.com/settings/tokens/new) with the `repo` scope. 35 | 36 | [Create a free Geocode account](https://geocode.xyz/api). 37 | 38 | ```bash 39 | # ------------------LangSmith tracing------------------ 40 | LANGCHAIN_API_KEY=... 41 | LANGCHAIN_CALLBACKS_BACKGROUND=true 42 | LANGCHAIN_TRACING_V2=true 43 | # ----------------------------------------------------- 44 | 45 | GITHUB_TOKEN=... 46 | OPENAI_API_KEY=... 47 | GEOCODE_API_KEY=... 48 | FIRECRAWL_API_KEY=... 49 | ``` 50 | 51 | ### Running the Application 52 | 53 | To run the application in development mode run: 54 | 55 | ```bash 56 | yarn dev 57 | ``` 58 | 59 | This will start the application on [`http://localhost:3000`](http://localhost:3000). 60 | 61 | To run in production mode: 62 | 63 | ```bash 64 | yarn start 65 | 66 | yarn build 67 | ``` 68 | 69 | ### Go further 70 | 71 | If you're interested in ways to take this demo application further, I'd consider the following: 72 | 73 | - Generating entire React components to be rendered, instead of relying on pre-built components. OR: Using the LLM to build custom components using a UI library like [Shadcn](https://ui.shadcn.com/). 74 | - Multi-tool and component usage. Update the LangGraph agent to call multiple tools, and appending multiple different UI components to the client rendered UI. 75 | - Generative UI outside of the chatbot window. Have the UI dynamically render in different areas on the screen. E.g a dashboard, where the components are dynamically rendered based on the LLMs output. 76 | -------------------------------------------------------------------------------- /ai/graph.tsx: -------------------------------------------------------------------------------- 1 | import { BaseMessage } from "@langchain/core/messages"; 2 | import { RunnableConfig } from "@langchain/core/runnables"; 3 | import { StateGraph, START, END } from "@langchain/langgraph"; 4 | import { 5 | ChatPromptTemplate, 6 | MessagesPlaceholder, 7 | } from "@langchain/core/prompts"; 8 | import { githubTool, invoiceTool, weatherTool, websiteDataTool } from "./tools"; 9 | import { ChatOpenAI } from "@langchain/openai"; 10 | 11 | interface AgentExecutorState { 12 | input: string; 13 | chat_history: BaseMessage[]; 14 | /** 15 | * The plain text result of the LLM if 16 | * no tool was used. 17 | */ 18 | result?: string; 19 | /** 20 | * The parsed tool result that was called. 21 | */ 22 | toolCall?: { 23 | name: string; 24 | parameters: Record; 25 | }; 26 | /** 27 | * The result of a tool. 28 | */ 29 | toolResult?: Record; 30 | } 31 | 32 | const invokeModel = async ( 33 | state: AgentExecutorState, 34 | config?: RunnableConfig, 35 | ): Promise> => { 36 | const initialPrompt = ChatPromptTemplate.fromMessages([ 37 | [ 38 | "system", 39 | `You are a helpful assistant. You're provided a list of tools, and an input from the user.\n 40 | Your job is to determine whether or not you have a tool which can handle the users input, or respond with plain text.`, 41 | ], 42 | new MessagesPlaceholder({ 43 | variableName: "chat_history", 44 | optional: true, 45 | }), 46 | ["human", "{input}"], 47 | ]); 48 | 49 | const tools = [githubTool, invoiceTool, weatherTool, websiteDataTool]; 50 | 51 | const llm = new ChatOpenAI({ 52 | temperature: 0, 53 | model: "gpt-4o", 54 | streaming: true, 55 | }).bindTools(tools); 56 | const chain = initialPrompt.pipe(llm); 57 | const result = await chain.invoke( 58 | { 59 | input: state.input, 60 | chat_history: state.chat_history, 61 | }, 62 | config, 63 | ); 64 | 65 | if (result.tool_calls && result.tool_calls.length > 0) { 66 | return { 67 | toolCall: { 68 | name: result.tool_calls[0].name, 69 | parameters: result.tool_calls[0].args, 70 | }, 71 | }; 72 | } 73 | return { 74 | result: result.content as string, 75 | }; 76 | }; 77 | 78 | const invokeToolsOrReturn = (state: AgentExecutorState) => { 79 | if (state.toolCall) { 80 | return "invokeTools"; 81 | } 82 | if (state.result) { 83 | return END; 84 | } 85 | throw new Error("No tool call or result found."); 86 | }; 87 | 88 | const invokeTools = async ( 89 | state: AgentExecutorState, 90 | config?: RunnableConfig, 91 | ): Promise> => { 92 | if (!state.toolCall) { 93 | throw new Error("No tool call found."); 94 | } 95 | const toolMap = { 96 | [githubTool.name]: githubTool, 97 | [invoiceTool.name]: invoiceTool, 98 | [weatherTool.name]: weatherTool, 99 | [websiteDataTool.name]: websiteDataTool, 100 | }; 101 | 102 | const selectedTool = toolMap[state.toolCall.name]; 103 | if (!selectedTool) { 104 | throw new Error("No tool found in tool map."); 105 | } 106 | const toolResult = await selectedTool.invoke( 107 | state.toolCall.parameters, 108 | config, 109 | ); 110 | return { 111 | toolResult: JSON.parse(toolResult), 112 | }; 113 | }; 114 | 115 | export function agentExecutor() { 116 | const workflow = new StateGraph({ 117 | channels: { 118 | input: null, 119 | chat_history: null, 120 | result: null, 121 | toolCall: null, 122 | toolResult: null, 123 | }, 124 | }) 125 | .addNode("invokeModel", invokeModel) 126 | .addNode("invokeTools", invokeTools) 127 | .addConditionalEdges("invokeModel", invokeToolsOrReturn) 128 | .addEdge(START, "invokeModel") 129 | .addEdge("invokeTools", END); 130 | 131 | const graph = workflow.compile(); 132 | return graph; 133 | } 134 | -------------------------------------------------------------------------------- /ai/message.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AIMessageText } from "@/components/prebuilt/message"; 4 | import { StreamableValue, useStreamableValue } from "ai/rsc"; 5 | 6 | export function AIMessage(props: { value: StreamableValue }) { 7 | const [data] = useStreamableValue(props.value); 8 | 9 | if (!data) { 10 | return null; 11 | } 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /ai/tools/firecrawl.tsx: -------------------------------------------------------------------------------- 1 | import { WebLoading, Web } from "@/components/prebuilt/web"; 2 | import { createRunnableUI } from "@/utils/server"; 3 | import { DynamicStructuredTool } from "@langchain/core/tools"; 4 | import { FireCrawlLoader } from "@langchain/community/document_loaders/web/firecrawl"; 5 | import { z } from "zod"; 6 | 7 | export const webSchema = z.object({ 8 | url: z.string().describe("The url to scrape"), 9 | }); 10 | 11 | export async function webData(input: z.infer) { 12 | // Initialize the FireCrawlLoader 13 | const firecrawl = new FireCrawlLoader({ 14 | url: input.url, 15 | mode: "scrape", 16 | params: { 17 | pageOptions: { 18 | screenshot: true, 19 | }, 20 | }, 21 | apiKey: process.env.FIRECRAWL_API_KEY, 22 | }); 23 | 24 | const [scrapedData] = await firecrawl.load(); 25 | 26 | return { 27 | ...input, 28 | title: scrapedData.metadata.title, 29 | description: scrapedData.metadata.description, 30 | screenshot: scrapedData.metadata.screenshot, 31 | }; 32 | } 33 | 34 | export const websiteDataTool = new DynamicStructuredTool({ 35 | name: "get_web_data", 36 | description: "A tool to fetch the current website data, given a url.", 37 | schema: webSchema, 38 | func: async (input, config) => { 39 | const stream = await createRunnableUI(config, ); 40 | const data = await webData(input); 41 | stream.done(); 42 | return JSON.stringify(data, null); 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /ai/tools/github_repo.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Octokit } from "octokit"; 3 | import { DynamicStructuredTool } from "@langchain/core/tools"; 4 | import { createRunnableUI } from "@/utils/server"; 5 | import { Github, GithubLoading } from "@/components/prebuilt/github"; 6 | 7 | const githubRepoToolSchema = z.object({ 8 | owner: z.string().describe("The name of the repository owner."), 9 | repo: z.string().describe("The name of the repository."), 10 | }); 11 | 12 | async function githubRepoTool(input: z.infer) { 13 | if (!process.env.GITHUB_TOKEN) { 14 | throw new Error("Missing GITHUB_TOKEN secret."); 15 | } 16 | const githubClient = new Octokit({ 17 | auth: process.env.GITHUB_TOKEN, 18 | }); 19 | try { 20 | const repoData = await githubClient.request("GET /repos/{owner}/{repo}", { 21 | owner: input.owner, 22 | repo: input.repo, 23 | headers: { 24 | "X-GitHub-Api-Version": "2022-11-28", 25 | }, 26 | }); 27 | return { 28 | ...input, 29 | description: repoData.data.description ?? "", 30 | stars: repoData.data.stargazers_count, 31 | language: repoData.data.language ?? "", 32 | }; 33 | } catch (err) { 34 | console.error(err); 35 | return "There was an error fetching the repository. Please check the owner and repo names."; 36 | } 37 | } 38 | 39 | export const githubTool = new DynamicStructuredTool({ 40 | name: "github_repo", 41 | description: 42 | "A tool to fetch details of a Github repository. Given owner and repo names, this tool will return the repo description, stars, and primary language.", 43 | schema: githubRepoToolSchema, 44 | func: async (input, config) => { 45 | const stream = await createRunnableUI(config, ); 46 | const result = await githubRepoTool(input); 47 | if (typeof result === "string") { 48 | // Failed to parse, return error message 49 | stream.done(

{result}

); 50 | return result; 51 | } 52 | stream.done(); 53 | return JSON.stringify(result, null); 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /ai/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./github_repo"; 2 | export * from "./invoice"; 3 | export * from "./weather"; 4 | export * from "./firecrawl"; 5 | -------------------------------------------------------------------------------- /ai/tools/invoice.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import { InvoiceLoading, Invoice } from "@/components/prebuilt/invoice"; 4 | import { createRunnableUI } from "@/utils/server"; 5 | import { DynamicStructuredTool } from "@langchain/core/tools"; 6 | 7 | const LineItemSchema = z.object({ 8 | id: z 9 | .string() 10 | .default(uuidv4()) 11 | .describe("Unique identifier for the line item"), 12 | name: z.string().describe("Name or description of the line item"), 13 | quantity: z.number().int().positive().describe("Quantity of the line item"), 14 | price: z.number().positive().describe("Price per unit of the line item"), 15 | }); 16 | 17 | const ShippingAddressSchema = z.object({ 18 | name: z.string().describe("Name of the recipient"), 19 | street: z.string().describe("Street address for shipping"), 20 | city: z.string().describe("City for shipping"), 21 | state: z.string().describe("State or province for shipping"), 22 | zip: z.string().describe("ZIP or postal code for shipping"), 23 | }); 24 | 25 | const CustomerInfoSchema = z.object({ 26 | name: z.string().describe("Name of the customer"), 27 | email: z.string().email().describe("Email address of the customer"), 28 | phone: z.string().optional().describe("Phone number of the customer"), 29 | }); 30 | 31 | const PaymentInfoSchema = z.object({ 32 | cardType: z.string().describe("Type of credit card used for payment"), 33 | cardNumberLastFour: z 34 | .string() 35 | .describe("Last four digits of the credit card number"), 36 | }); 37 | 38 | export const InvoiceSchema = z.object({ 39 | orderId: z.string().describe("The order ID"), 40 | lineItems: z 41 | .array(LineItemSchema) 42 | .describe("List of line items in the invoice"), 43 | shippingAddress: ShippingAddressSchema.optional().describe( 44 | "Shipping address for the order", 45 | ), 46 | customerInfo: CustomerInfoSchema.optional().describe( 47 | "Information about the customer", 48 | ), 49 | paymentInfo: PaymentInfoSchema.optional().describe( 50 | "Payment information for the order", 51 | ), 52 | }); 53 | 54 | export const invoiceTool = new DynamicStructuredTool({ 55 | name: "get_order_invoice", 56 | description: 57 | "A tool to fetch the invoice from an order. This should only be called if a user uploads an image/receipt of an order.", 58 | schema: InvoiceSchema, 59 | func: async (input, config) => { 60 | const stream = await createRunnableUI(config, ); 61 | stream.done(); 62 | return JSON.stringify(input, null); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /ai/tools/weather.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CurrentWeatherLoading, 3 | CurrentWeather, 4 | } from "@/components/prebuilt/weather"; 5 | import { createRunnableUI } from "@/utils/server"; 6 | import { DynamicStructuredTool } from "@langchain/core/tools"; 7 | import { z } from "zod"; 8 | 9 | export const weatherSchema = z.object({ 10 | city: z.string().describe("The city name to get weather for"), 11 | state: z 12 | .string() 13 | .describe("The two letter state abbreviation to get weather for"), 14 | country: z 15 | .string() 16 | .optional() 17 | .default("usa") 18 | .describe("The two letter country abbreviation to get weather for"), 19 | }); 20 | 21 | export async function weatherData(input: z.infer) { 22 | const geoCodeApiKey = process.env.GEOCODE_API_KEY; 23 | if (!geoCodeApiKey) { 24 | throw new Error("Missing GEOCODE_API_KEY secret."); 25 | } 26 | 27 | const geoCodeResponse = await fetch( 28 | `https://geocode.xyz/${input.city.toLowerCase()},${input.state.toLowerCase()},${input.country.toLowerCase()}?json=1&auth=${geoCodeApiKey}`, 29 | ); 30 | if (!geoCodeResponse.ok) { 31 | console.error("No geocode data found."); 32 | throw new Error("Failed to get geocode data."); 33 | } 34 | const geoCodeData = await geoCodeResponse.json(); 35 | const { latt, longt } = geoCodeData; 36 | 37 | const weatherGovResponse = await fetch( 38 | `https://api.weather.gov/points/${latt},${longt}`, 39 | ); 40 | if (!weatherGovResponse.ok) { 41 | console.error("No weather data found."); 42 | throw new Error("Failed to get weather data."); 43 | } 44 | const weatherGovData = await weatherGovResponse.json(); 45 | const { properties } = weatherGovData; 46 | 47 | const forecastResponse = await fetch(properties.forecast); 48 | if (!forecastResponse.ok) { 49 | console.error("No forecast data found."); 50 | throw new Error("Failed to get forecast data."); 51 | } 52 | const forecastData = await forecastResponse.json(); 53 | const { periods } = forecastData.properties; 54 | const todayForecast = periods[0]; 55 | 56 | return { 57 | ...input, 58 | temperature: todayForecast.temperature, 59 | }; 60 | } 61 | 62 | export const weatherTool = new DynamicStructuredTool({ 63 | name: "get_weather", 64 | description: 65 | "A tool to fetch the current weather, given a city and state. If the city/state is not provided, ask the user for both the city and state.", 66 | schema: weatherSchema, 67 | func: async (input, config) => { 68 | const stream = await createRunnableUI(config, ); 69 | const data = await weatherData(input); 70 | stream.done(); 71 | return JSON.stringify(data, null); 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /app/agent.tsx: -------------------------------------------------------------------------------- 1 | import { agentExecutor } from "@/ai/graph"; 2 | import { exposeEndpoints, streamRunnableUI } from "@/utils/server"; 3 | import { HumanMessage, AIMessage } from "@langchain/core/messages"; 4 | 5 | const convertChatHistoryToMessages = ( 6 | chat_history: [role: string, content: string][], 7 | ) => { 8 | return chat_history.map(([role, content]) => { 9 | switch (role) { 10 | case "human": 11 | return new HumanMessage(content); 12 | case "assistant": 13 | case "ai": 14 | return new AIMessage(content); 15 | default: 16 | return new HumanMessage(content); 17 | } 18 | }); 19 | }; 20 | 21 | function processFile(input: { 22 | input: string; 23 | chat_history: [role: string, content: string][]; 24 | file?: { 25 | base64: string; 26 | extension: string; 27 | }; 28 | }) { 29 | if (input.file) { 30 | const imageTemplate = new HumanMessage({ 31 | content: [ 32 | { 33 | type: "image_url", 34 | image_url: { 35 | url: `data:image/${input.file.extension};base64,${input.file.base64}`, 36 | }, 37 | }, 38 | ], 39 | }); 40 | return { 41 | input: input.input, 42 | chat_history: [ 43 | ...convertChatHistoryToMessages(input.chat_history), 44 | imageTemplate, 45 | ], 46 | }; 47 | } else { 48 | return { 49 | input: input.input, 50 | chat_history: convertChatHistoryToMessages(input.chat_history), 51 | }; 52 | } 53 | } 54 | 55 | async function agent(inputs: { 56 | input: string; 57 | chat_history: [role: string, content: string][]; 58 | file?: { 59 | base64: string; 60 | extension: string; 61 | }; 62 | }) { 63 | "use server"; 64 | const processedInputs = processFile(inputs); 65 | 66 | return streamRunnableUI(agentExecutor(), processedInputs); 67 | } 68 | 69 | export const EndpointsContext = exposeEndpoints({ agent }); 70 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/gen-ui-firecrawl/2354365ae04d775f1ad1eba773febdb277abe6c5/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .dot-typing { 79 | position: relative; 80 | left: -9999px; 81 | width: 10px; 82 | height: 10px; 83 | border-radius: 5px; 84 | background-color: #fff; 85 | color: #fff; 86 | box-shadow: 87 | 9984px 0 0 0 #fff, 88 | 9999px 0 0 0 #fff, 89 | 10014px 0 0 0 #fff; 90 | animation: dotTyping 1.5s infinite linear; 91 | } 92 | 93 | @keyframes dotTyping { 94 | 0% { 95 | box-shadow: 96 | 9984px 0 0 0 #fff, 97 | 9999px 0 0 0 #fff, 98 | 10014px 0 0 0 #fff; 99 | } 100 | 16.667% { 101 | box-shadow: 102 | 9984px -10px 0 0 #fff, 103 | 9999px 0 0 0 #fff, 104 | 10014px 0 0 0 #fff; 105 | } 106 | 33.333% { 107 | box-shadow: 108 | 9984px 0 0 0 #fff, 109 | 9999px 0 0 0 #fff, 110 | 10014px 0 0 0 #fff; 111 | } 112 | 50% { 113 | box-shadow: 114 | 9984px 0 0 0 #fff, 115 | 9999px -10px 0 0 #fff, 116 | 10014px 0 0 0 #fff; 117 | } 118 | 66.667% { 119 | box-shadow: 120 | 9984px 0 0 0 #fff, 121 | 9999px 0 0 0 #fff, 122 | 10014px 0 0 0 #fff; 123 | } 124 | 83.333% { 125 | box-shadow: 126 | 9984px 0 0 0 #fff, 127 | 9999px 0 0 0 #fff, 128 | 10014px -10px 0 0 #fff; 129 | } 130 | 100% { 131 | box-shadow: 132 | 9984px 0 0 0 #fff, 133 | 9999px 0 0 0 #fff, 134 | 10014px 0 0 0 #fff; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | 4 | import { EndpointsContext } from "./agent"; 5 | import { ReactNode } from "react"; 6 | 7 | export const metadata: Metadata = { 8 | title: "LangChain.js Gen UI", 9 | description: "Generative UI application with LangChain.js", 10 | }; 11 | 12 | export default function RootLayout(props: { children: ReactNode }) { 13 | return ( 14 | 15 | 16 |
17 | {props.children} 18 |
19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Chat from "@/components/prebuilt/chat"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 |

8 | Generative UI with{" "} 9 | 14 | LangChain.js 🦜🔗 15 | 16 |

17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/shared.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createContext } from "react"; 3 | 4 | export const LocalContext = createContext<(value: string) => void>( 5 | () => void 0, 6 | ); 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/prebuilt/chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Input } from "../ui/input"; 5 | import { Button } from "../ui/button"; 6 | import { EndpointsContext } from "@/app/agent"; 7 | import { useActions } from "@/utils/client"; 8 | import { LocalContext } from "@/app/shared"; 9 | import { HumanMessageText } from "./message"; 10 | 11 | export interface ChatProps {} 12 | 13 | function convertFileToBase64(file: File): Promise { 14 | return new Promise((resolve, reject) => { 15 | const reader = new FileReader(); 16 | reader.onload = () => { 17 | const base64String = reader.result as string; 18 | resolve(base64String.split(",")[1]); // Remove the data URL prefix 19 | }; 20 | reader.onerror = (error) => { 21 | reject(error); 22 | }; 23 | reader.readAsDataURL(file); 24 | }); 25 | } 26 | 27 | function FileUploadMessage({ file }: { file: File }) { 28 | return ( 29 |
30 |

File uploaded: {file.name}

31 |
32 | ); 33 | } 34 | 35 | export default function Chat() { 36 | const actions = useActions(); 37 | 38 | const [elements, setElements] = useState([]); 39 | const [history, setHistory] = useState<[role: string, content: string][]>([]); 40 | const [input, setInput] = useState(""); 41 | const [selectedFile, setSelectedFile] = useState(); 42 | 43 | async function onSubmit(input: string) { 44 | const newElements = [...elements]; 45 | let base64File: string | undefined = undefined; 46 | let fileExtension = selectedFile?.type.split("/")[1]; 47 | if (selectedFile) { 48 | base64File = await convertFileToBase64(selectedFile); 49 | } 50 | 51 | const element = await actions.agent({ 52 | input, 53 | chat_history: history, 54 | file: 55 | base64File && fileExtension 56 | ? { 57 | base64: base64File, 58 | extension: fileExtension, 59 | } 60 | : undefined, 61 | }); 62 | 63 | newElements.push( 64 |
65 | {selectedFile && } 66 | 67 |
68 | {element.ui} 69 |
70 |
, 71 | ); 72 | 73 | // consume the value stream to obtain the final value 74 | // after which we can append to our chat history state 75 | (async () => { 76 | let lastEvent = await element.lastEvent; 77 | if (typeof lastEvent === "object") { 78 | if (lastEvent["invokeModel"]["result"]) { 79 | setHistory((prev) => [ 80 | ...prev, 81 | ["user", input], 82 | ["assistant", lastEvent["invokeModel"]["result"]], 83 | ]); 84 | } else if (lastEvent["invokeTools"]) { 85 | setHistory((prev) => [ 86 | ...prev, 87 | ["user", input], 88 | [ 89 | "assistant", 90 | `Tool result: ${JSON.stringify(lastEvent["invokeTools"]["toolResult"], null)}`, 91 | ], 92 | ]); 93 | } else { 94 | console.log("ELSE!", lastEvent); 95 | } 96 | } 97 | })(); 98 | 99 | setElements(newElements); 100 | setInput(""); 101 | setSelectedFile(undefined); 102 | } 103 | 104 | return ( 105 |
106 | 107 |
{elements}
108 |
109 |
{ 111 | e.stopPropagation(); 112 | e.preventDefault(); 113 | await onSubmit(input); 114 | }} 115 | className="w-full flex flex-row gap-2" 116 | > 117 | setInput(e.target.value)} 121 | /> 122 |
123 | { 129 | if (e.target.files && e.target.files.length > 0) { 130 | setSelectedFile(e.target.files[0]); 131 | } 132 | }} 133 | /> 134 |
135 | 136 |
137 |
138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /components/prebuilt/github.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CircleIcon, StarIcon } from "@radix-ui/react-icons"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardHeader, 11 | CardTitle, 12 | } from "@/components/ui/card"; 13 | import { Skeleton } from "../ui/skeleton"; 14 | import { format } from "date-fns"; 15 | 16 | export interface DemoGithubProps { 17 | owner: string; 18 | repo: string; 19 | description: string; 20 | stars: number; 21 | language: string; 22 | } 23 | 24 | export function GithubLoading() { 25 | return ( 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 |
34 | {Array.from({ length: 3 }).map((_, i) => ( 35 | 39 | ))} 40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 |
51 | ); 52 | } 53 | 54 | export function Github(props: DemoGithubProps) { 55 | const currentMonth = format(new Date(), "MMMM"); 56 | const currentYear = format(new Date(), "yyyy"); 57 | return ( 58 | 59 | 60 |
61 | 62 | {props.owner}/{props.repo} 63 | 64 | {props.description} 65 |
66 | 79 |
80 | 81 |
82 |
83 | 84 | {props.language} 85 |
86 |
87 | 88 | {props.stars.toLocaleString()} 89 |
90 |
91 | Updated {currentMonth} {currentYear} 92 |
93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/prebuilt/invoice.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Copy, CreditCard, MoreVertical, Truck } from "lucide-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card"; 14 | import { 15 | DropdownMenu, 16 | DropdownMenuContent, 17 | DropdownMenuItem, 18 | DropdownMenuSeparator, 19 | DropdownMenuTrigger, 20 | } from "@/components/ui/dropdown-menu"; 21 | import { Separator } from "@/components/ui/separator"; 22 | import { useEffect, useState } from "react"; 23 | import { Skeleton } from "../ui/skeleton"; 24 | import { format } from "date-fns"; 25 | 26 | export type LineItem = { 27 | id: string; 28 | name: string; 29 | quantity: number; 30 | price: number; 31 | }; 32 | 33 | export type ShippingAddress = { 34 | name: string; 35 | street: string; 36 | city: string; 37 | state: string; 38 | zip: string; 39 | }; 40 | 41 | export type CustomerInfo = { 42 | name: string; 43 | email?: string; 44 | phone?: string; 45 | }; 46 | 47 | export type PaymentInfo = { 48 | cardType: string; 49 | cardNumberLastFour: string; 50 | }; 51 | 52 | export interface InvoiceProps { 53 | orderId: string; 54 | lineItems: LineItem[]; 55 | shippingAddress?: ShippingAddress; 56 | customerInfo?: CustomerInfo; 57 | paymentInfo?: PaymentInfo; 58 | } 59 | 60 | export function InvoiceLoading() { 61 | return ( 62 | 63 | 64 |
65 |
66 | 67 |
68 |
Order Details
69 |
    70 | {Array.from({ length: 3 }).map((_, i) => ( 71 |
  • 75 | 76 | 77 | 78 | 79 | 80 | 81 |
  • 82 | ))} 83 |
84 | 85 |
    86 |
  • 87 | Subtotal 88 | 89 | 90 | 91 |
  • 92 |
  • 93 | Shipping 94 | 95 | 96 | 97 |
  • 98 |
  • 99 | Tax 100 | 101 | 102 | 103 |
  • 104 |
  • 105 | Total 106 | 107 | 108 | 109 |
  • 110 |
111 |
112 | 113 |
114 |
115 |
Shipping Information
116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 |
128 |
129 |
Billing Information
130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 |
142 |
143 | 144 |
145 |
Customer Information
146 |
147 |
148 |
Customer
149 |
150 | 151 |
152 |
153 |
154 |
Email
155 |
156 | 157 | 158 | 159 |
160 |
161 |
162 |
Phone
163 |
164 | 165 | 166 | 167 |
168 |
169 |
170 |
171 | 172 |
173 |
Payment Information
174 |
175 |
176 |
177 | 178 | 179 |
180 |
181 | 182 |
183 |
184 |
185 |
186 |
187 | 188 |
189 | Updated 190 |
191 |
192 |
193 | ); 194 | } 195 | 196 | export function Invoice(props: InvoiceProps) { 197 | const [priceDetails, setPriceDetails] = useState({ 198 | shipping: 5.0, 199 | tax: 0.0, 200 | total: 0.0, 201 | lineItemTotal: 0.0, 202 | }); 203 | const currentMonth = format(new Date(), "MMMM"); 204 | const currentDay = format(new Date(), "EEEE"); 205 | const currentYear = format(new Date(), "yyyy"); 206 | 207 | useEffect(() => { 208 | if (props.lineItems.length > 0) { 209 | const totalPriceLineItems = props.lineItems 210 | .reduce((acc, lineItem) => { 211 | return acc + lineItem.price * lineItem.quantity; 212 | }, 0) 213 | .toFixed(2); 214 | const shipping = 5.0; 215 | const tax = Number(totalPriceLineItems) * 0.075; 216 | const total = (Number(totalPriceLineItems) + shipping + tax).toFixed(2); 217 | setPriceDetails({ 218 | shipping, 219 | tax, 220 | total: Number(total), 221 | lineItemTotal: Number(totalPriceLineItems), 222 | }); 223 | } 224 | }, [props.lineItems]); 225 | 226 | return ( 227 | 228 | 229 |
230 | 231 | Order {props.orderId} 232 | 240 | 241 | 242 | Date: {currentMonth} {currentDay}, {currentYear} 243 | 244 |
245 |
246 | 252 | 253 | 254 | 258 | 259 | 260 | Edit 261 | Export 262 | 263 | Trash 264 | 265 | 266 |
267 |
268 | 269 |
270 |
Order Details
271 |
    272 | {props.lineItems.map((lineItem) => { 273 | const totalPrice = (lineItem.price * lineItem.quantity).toFixed( 274 | 2, 275 | ); 276 | return ( 277 |
  • 281 | 282 | {lineItem.name} x {lineItem.quantity} 283 | 284 | ${totalPrice} 285 |
  • 286 | ); 287 | })} 288 |
289 | 290 |
    291 |
  • 292 | Subtotal 293 | ${priceDetails.lineItemTotal} 294 |
  • 295 |
  • 296 | Shipping 297 | ${priceDetails.shipping} 298 |
  • 299 |
  • 300 | Tax 301 | ${priceDetails.tax} 302 |
  • 303 |
  • 304 | Total 305 | ${priceDetails.total} 306 |
  • 307 |
308 |
309 | 310 | {props.shippingAddress && ( 311 | <> 312 | 313 |
314 |
315 |
Shipping Information
316 |
317 | {props.shippingAddress.name} 318 | {props.shippingAddress.street} 319 | 320 | {props.shippingAddress.city} {props.shippingAddress.state},{" "} 321 | {props.shippingAddress.zip} 322 | 323 |
324 |
325 |
326 |
Billing Information
327 |
328 | Same as shipping address 329 |
330 |
331 |
332 | 333 | )} 334 | 335 | {props.customerInfo && ( 336 | <> 337 | 338 |
339 |
Customer Information
340 |
341 |
342 |
Customer
343 |
{props.customerInfo.name}
344 |
345 | {props.customerInfo.email && ( 346 |
347 |
Email
348 |
349 | {props.customerInfo.email} 350 |
351 |
352 | )} 353 | {props.customerInfo.phone && ( 354 |
355 |
Phone
356 |
357 | {props.customerInfo.phone} 358 |
359 |
360 | )} 361 |
362 |
363 | 364 | )} 365 | 366 | {props.paymentInfo && ( 367 | <> 368 | 369 |
370 |
Payment Information
371 |
372 |
373 |
374 | 375 | {props.paymentInfo.cardType} 376 |
377 |
**** **** **** {props.paymentInfo.cardNumberLastFour}
378 |
379 |
380 |
381 | 382 | )} 383 |
384 | 385 |
386 | Updated 387 |
388 |
389 |
390 | ); 391 | } 392 | -------------------------------------------------------------------------------- /components/prebuilt/message.tsx: -------------------------------------------------------------------------------- 1 | import Markdown from "react-markdown"; 2 | 3 | export interface MessageTextProps { 4 | content: string; 5 | } 6 | 7 | export function AIMessageText(props: MessageTextProps) { 8 | return ( 9 |
10 |

11 | {props.content} 12 |

13 |
14 | ); 15 | } 16 | 17 | export function HumanMessageText(props: MessageTextProps) { 18 | return ( 19 |
20 |

21 | {props.content} 22 |

23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/prebuilt/weather.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card } from "../ui/card"; 4 | import { format } from "date-fns"; 5 | import { Progress } from "../ui/progress"; 6 | import { Skeleton } from "../ui/skeleton"; 7 | 8 | export interface CurrentWeatherProps { 9 | temperature: number; 10 | city: string; 11 | state: string; 12 | } 13 | 14 | export function CurrentWeatherLoading() { 15 | return ( 16 | 17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | ); 36 | } 37 | 38 | export function CurrentWeather(props: CurrentWeatherProps) { 39 | const currentTime = format(new Date(), "hh:mm:ss a"); 40 | const currentDay = format(new Date(), "EEEE"); 41 | // assume the maximum temperature is 130 and the minium is -20 42 | const weatherAsPercentage = (props.temperature + 20) / 150; 43 | return ( 44 | 45 |
46 |

{currentDay}

47 |

{currentTime}

48 |
49 |
50 |

51 | {props.city}, {props.state} 52 |

53 |
54 |
55 |
56 |

{props.temperature}°

57 |
58 |
59 |
60 | 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/prebuilt/web.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card } from "../ui/card"; 4 | import { Skeleton } from "../ui/skeleton"; 5 | 6 | export interface WebProps { 7 | url: string; 8 | title: string; 9 | description: string; 10 | screenshot: string; 11 | } 12 | 13 | export function WebLoading() { 14 | return ( 15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | export function Web(props: WebProps) { 30 | const cleanUrl = props.url 31 | .replace("https://", "") 32 | .replace("http://", "") 33 | .replace("www.", ""); 34 | return ( 35 | 36 | 37 |
38 |

39 | {props.title} ({cleanUrl}) 40 |

41 |
42 |
43 | The screenshot of the website 49 |
50 |
51 |

{props.description}

52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full 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", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ChevronLeft, ChevronRight } from "lucide-react"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { buttonVariants } from "@/components/ui/button"; 9 | 10 | export type CalendarProps = React.ComponentProps; 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ); 63 | } 64 | Calendar.displayName = "Calendar"; 65 | 66 | export { Calendar }; 67 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName; 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName; 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )); 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean; 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )); 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )); 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName; 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )); 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean; 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )); 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )); 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ); 181 | }; 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | }; 201 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /components/ui/pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"; 6 | 7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( 8 |