├── .codespellignore ├── .prettierignore ├── .yarnrc.yml ├── .dockerignore ├── .prettierrc ├── src ├── lib │ ├── constants.ts │ ├── utils.ts │ └── local-storage.ts ├── app │ ├── favicon.ico │ ├── api │ │ ├── [..._path] │ │ │ └── route.ts │ │ └── instance │ │ │ ├── status │ │ │ └── route.ts │ │ │ ├── stop │ │ │ └── route.ts │ │ │ ├── pause │ │ │ └── route.ts │ │ │ └── resume │ │ │ └── route.ts │ ├── page.tsx │ ├── layout.tsx │ └── globals.css ├── providers │ ├── client.ts │ ├── Thread.tsx │ └── Stream.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── password-input.tsx │ │ ├── card.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── alert-dialog.tsx │ │ └── sheet.tsx │ ├── thread │ │ ├── utils.ts │ │ ├── markdown-styles.css │ │ ├── tooltip-icon-button.tsx │ │ ├── syntax-highlighter.tsx │ │ ├── messages │ │ │ ├── human.tsx │ │ │ ├── ai.tsx │ │ │ ├── shared.tsx │ │ │ └── tool-calls.tsx │ │ ├── history │ │ │ └── index.tsx │ │ ├── markdown-text.tsx │ │ └── index.tsx │ └── icons │ │ └── langgraph.tsx ├── agent │ ├── ui │ │ ├── index.tsx │ │ ├── render-vm-button.tsx │ │ ├── instance │ │ │ ├── instance-view.tsx │ │ │ ├── window-manager-buttons.tsx │ │ │ ├── useInstanceActions.tsx │ │ │ └── index.tsx │ │ ├── computer-use-tool-output.tsx │ │ ├── styles.css │ │ └── computer-use-tool-call.tsx │ ├── styles.css │ └── index.ts └── hooks │ └── useMediaQuery.tsx ├── postcss.config.mjs ├── public ├── gen_ui_diagram.png └── logo.svg ├── next.config.mjs ├── langgraph.json ├── .gitignore ├── components.json ├── .env.example ├── tsconfig.json ├── eslint.config.js ├── LICENSE ├── README.md ├── .github └── workflows │ └── ci.yml ├── tailwind.config.js └── package.json /.codespellignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .git 4 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-"; 2 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bracesproul/gen-ui-computer-use/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/gen_ui_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bracesproul/gen-ui-computer-use/HEAD/public/gen_ui_diagram.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /src/providers/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@langchain/langgraph-sdk"; 2 | 3 | export function createClient(apiUrl: string) { 4 | return new Client({ 5 | apiUrl, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/app/api/[..._path]/route.ts: -------------------------------------------------------------------------------- 1 | import { initApiPassthrough } from "langgraph-nextjs-api-passthrough"; 2 | 3 | export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } = 4 | initApiPassthrough({ 5 | apiUrl: process.env.LANGGRAPH_API_URL, 6 | apiKey: process.env.LANGCHAIN_API_KEY, 7 | runtime: "edge" as const, // default 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /src/components/thread/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "@langchain/langgraph-sdk"; 2 | 3 | export function getContentString(content: Message["content"]): string { 4 | if (typeof content === "string") return content; 5 | const texts = content 6 | .filter((c): c is { type: "text"; text: string } => c.type === "text") 7 | .map((c) => c.text); 8 | return texts.join(" "); 9 | } 10 | -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "graphs": { 4 | "agent": "./src/agent/index.ts:graph" 5 | }, 6 | "ui": { 7 | "agent": "./src/agent/ui/index.tsx" 8 | }, 9 | "ui_config": { 10 | "shared": [ 11 | "nuqs", 12 | "nuqs/adapters/next/app", 13 | "@/components/ui/sonner", 14 | "sonner" 15 | ] 16 | }, 17 | "env": ".env", 18 | "dependencies": ["."] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # LangGraph API 27 | .langgraph_api 28 | .env 29 | .yarn/ 30 | !.yarn/install-state.gz 31 | .next/ 32 | next-env.d.ts -------------------------------------------------------------------------------- /src/agent/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComputerUseToolCall } from "./computer-use-tool-call"; 2 | import { ComputerUseToolOutput } from "./computer-use-tool-output"; 3 | import { RenderVMButton } from "./render-vm-button"; 4 | import { InstanceFrame } from "./instance"; 5 | 6 | const ComponentMap = { 7 | "computer-use-tool-output": ComputerUseToolOutput, 8 | "computer-use-tool-call": ComputerUseToolCall, 9 | "render-vm-button": RenderVMButton, 10 | instance: InstanceFrame, 11 | } as const; 12 | export default ComponentMap; 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 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 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [matches, setMatches] = useState(false); 5 | 6 | useEffect(() => { 7 | const media = window.matchMedia(query); 8 | setMatches(media.matches); 9 | 10 | const listener = (e: MediaQueryListEvent) => setMatches(e.matches); 11 | media.addEventListener("change", listener); 12 | return () => media.removeEventListener("change", listener); 13 | }, [query]); 14 | 15 | return matches; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/local-storage.ts: -------------------------------------------------------------------------------- 1 | export const USER_ID_KEY = "gen_ui_cua_user_id"; 2 | 3 | export function setItem(key: string, value: string) { 4 | if (typeof window === "undefined") { 5 | return; 6 | } 7 | localStorage.setItem(key, value); 8 | } 9 | 10 | export function getItem(key: string) { 11 | if (typeof window === "undefined") { 12 | return undefined; 13 | } 14 | return localStorage.getItem(key) ?? undefined; 15 | } 16 | 17 | export function removeItem(key: string) { 18 | if (typeof window === "undefined") { 19 | return; 20 | } 21 | localStorage.removeItem(key); 22 | } 23 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ------------------LangSmith tracing------------------ 2 | # LANGCHAIN_PROJECT="default" 3 | # LANGCHAIN_API_KEY="" 4 | # LANGCHAIN_TRACING_V2=true 5 | # ----------------------------------------------------- 6 | 7 | OPENAI_API_KEY="" 8 | SCRAPYBARA_API_KEY="" 9 | 10 | # Local URL for development. This should be your LangGraph Cloud URL 11 | # when setting this value in production. 12 | LANGGRAPH_API_URL="http://localhost:2024" 13 | 14 | # Local URL for development. This should be your domain name + "/api" 15 | # when setting this value in production. 16 | NEXT_PUBLIC_API_URL="http://localhost:3000/api" 17 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Thread } from "@/components/thread"; 4 | import { StreamProvider } from "@/providers/Stream"; 5 | import { ThreadProvider } from "@/providers/Thread"; 6 | import { Toaster } from "@/components/ui/sonner"; 7 | import React from "react"; 8 | 9 | export default function DemoPage(): React.ReactNode { 10 | return ( 11 | Loading (layout)...
}> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { Inter } from "next/font/google"; 4 | import React from "react"; 5 | import { NuqsAdapter } from "nuqs/adapters/next/app"; 6 | 7 | const inter = Inter({ 8 | subsets: ["latin"], 9 | preload: true, 10 | display: "swap", 11 | }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Generative UI CUA", 15 | description: "Generative UI Computer Use Agent by LangChain", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ); 24 | } 25 | 26 | export { Separator }; 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |