├── .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 |
15 | );
16 | }
17 |
18 | export { Textarea };
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "target": "ES2017"
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | "src/hooks/use-threads",
31 | "src/app/thread/[threadId]"
32 | ],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/src/agent/ui/render-vm-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import "./styles.css";
4 | import { Button } from "@/components/ui/button";
5 | import { ComputerIcon } from "lucide-react";
6 | import { useQueryState, parseAsBoolean } from "nuqs";
7 |
8 | export function RenderVMButton() {
9 | const [isShowingInstanceFrame, setIsShowingInstanceFrame] = useQueryState(
10 | "isShowingInstanceFrame",
11 | parseAsBoolean,
12 | );
13 |
14 | const onClick = () => {
15 | setIsShowingInstanceFrame((p) => !p);
16 | };
17 |
18 | return (
19 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
17 | );
18 | }
19 |
20 | export { Input };
21 |
--------------------------------------------------------------------------------
/src/components/thread/markdown-styles.css:
--------------------------------------------------------------------------------
1 | /* Base markdown styles */
2 | .markdown-content code:not(pre code) {
3 | background-color: rgba(0, 0, 0, 0.05);
4 | padding: 0.2em 0.4em;
5 | border-radius: 3px;
6 | font-size: 0.9em;
7 | }
8 |
9 | .markdown-content a {
10 | color: #0070f3;
11 | text-decoration: none;
12 | }
13 |
14 | .markdown-content a:hover {
15 | text-decoration: underline;
16 | }
17 |
18 | .markdown-content blockquote {
19 | border-left: 4px solid #ddd;
20 | padding-left: 1rem;
21 | color: #666;
22 | }
23 |
24 | .markdown-content pre {
25 | overflow-x: auto;
26 | }
27 |
28 | .markdown-content table {
29 | border-collapse: collapse;
30 | width: 100%;
31 | }
32 |
33 | .markdown-content th,
34 | .markdown-content td {
35 | border: 1px solid #ddd;
36 | padding: 8px;
37 | }
38 |
39 | .markdown-content th {
40 | background-color: #f2f2f2;
41 | }
42 |
43 | .markdown-content tr:nth-child(even) {
44 | background-color: #f9f9f9;
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { Toaster as Sonner, ToasterProps } from "sonner";
3 |
4 | const Toaster = ({ ...props }: ToasterProps) => {
5 | const { theme = "system" } = useTheme();
6 |
7 | return (
8 |
24 | );
25 | };
26 |
27 | export { Toaster };
28 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "@typescript-eslint/no-explicit-any": 0,
23 | "@typescript-eslint/no-unused-vars": [
24 | "warn",
25 | { args: "none", argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
26 | ],
27 | "react-refresh/only-export-components": [
28 | "warn",
29 | { allowConstantExport: true },
30 | ],
31 | },
32 | },
33 | );
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Brace Sproul
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SwitchPrimitive from "@radix-ui/react-switch";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | function Switch({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 |
25 |
26 | );
27 | }
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/app/api/instance/status/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { ScrapybaraClient } from "scrapybara";
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const body = await req.json();
7 | const { instanceId } = body as { instanceId: string };
8 |
9 | if (!instanceId) {
10 | return NextResponse.json(
11 | { error: "`instanceId` is required." },
12 | { status: 400 },
13 | );
14 | }
15 |
16 | if (!process.env.SCRAPYBARA_API_KEY) {
17 | return NextResponse.json(
18 | {
19 | error: "Scrapybara API key is missing",
20 | },
21 | { status: 400 },
22 | );
23 | }
24 | const client = new ScrapybaraClient({
25 | apiKey: process.env.SCRAPYBARA_API_KEY,
26 | });
27 |
28 | const instance = await client.get(instanceId);
29 |
30 | return NextResponse.json({ status: instance.status }, { status: 200 });
31 | } catch (error: any) {
32 | console.error("Failed to process stop instance request:", error);
33 |
34 | return NextResponse.json(
35 | { error: "Failed to stop instance." + error.message },
36 | { status: 500 },
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/api/instance/stop/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { ScrapybaraClient } from "scrapybara";
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const body = await req.json();
7 | const { instanceId } = body as { instanceId: string };
8 |
9 | if (!instanceId) {
10 | return NextResponse.json(
11 | { error: "`instanceId` is required." },
12 | { status: 400 },
13 | );
14 | }
15 |
16 | if (!process.env.SCRAPYBARA_API_KEY) {
17 | return NextResponse.json(
18 | {
19 | error: "Scrapybara API key is missing",
20 | },
21 | { status: 400 },
22 | );
23 | }
24 | const client = new ScrapybaraClient({
25 | apiKey: process.env.SCRAPYBARA_API_KEY,
26 | });
27 |
28 | const instance = await client.get(instanceId);
29 | await instance.stop();
30 |
31 | return NextResponse.json({ success: true }, { status: 200 });
32 | } catch (error: any) {
33 | console.error("Failed to process stop instance request:", error);
34 |
35 | return NextResponse.json(
36 | { error: "Failed to stop instance." + error.message },
37 | { status: 500 },
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/api/instance/pause/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { ScrapybaraClient } from "scrapybara";
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const body = await req.json();
7 | const { instanceId } = body as { instanceId: string };
8 |
9 | if (!instanceId) {
10 | return NextResponse.json(
11 | { error: "`instanceId` is required." },
12 | { status: 400 },
13 | );
14 | }
15 |
16 | if (!process.env.SCRAPYBARA_API_KEY) {
17 | return NextResponse.json(
18 | {
19 | error: "Scrapybara API key is missing",
20 | },
21 | { status: 400 },
22 | );
23 | }
24 | const client = new ScrapybaraClient({
25 | apiKey: process.env.SCRAPYBARA_API_KEY,
26 | });
27 |
28 | const instance = await client.get(instanceId);
29 | await instance.pause();
30 |
31 | return NextResponse.json({ success: true }, { status: 200 });
32 | } catch (error: any) {
33 | console.error("Failed to process pause instance request:", error);
34 |
35 | return NextResponse.json(
36 | { error: "Failed to pause instance." + error.message },
37 | { status: 500 },
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/api/instance/resume/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { ScrapybaraClient } from "scrapybara";
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const body = await req.json();
7 | const { instanceId } = body as { instanceId: string };
8 |
9 | if (!instanceId) {
10 | return NextResponse.json(
11 | { error: "`instanceId` is required." },
12 | { status: 400 },
13 | );
14 | }
15 |
16 | if (!process.env.SCRAPYBARA_API_KEY) {
17 | return NextResponse.json(
18 | {
19 | error: "Scrapybara API key is missing",
20 | },
21 | { status: 400 },
22 | );
23 | }
24 | const client = new ScrapybaraClient({
25 | apiKey: process.env.SCRAPYBARA_API_KEY,
26 | });
27 |
28 | const instance = await client.get(instanceId);
29 | await instance.resume();
30 |
31 | return NextResponse.json({ success: true }, { status: 200 });
32 | } catch (error: any) {
33 | console.error("Failed to process resume instance request:", error);
34 |
35 | return NextResponse.json(
36 | { error: "Failed to resume instance." + error.message },
37 | { status: 500 },
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/thread/tooltip-icon-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { forwardRef } from "react";
4 |
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipProvider,
9 | TooltipTrigger,
10 | } from "@/components/ui/tooltip";
11 | import { Button, ButtonProps } from "@/components/ui/button";
12 | import { cn } from "@/lib/utils";
13 |
14 | export type TooltipIconButtonProps = ButtonProps & {
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 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | function Avatar({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | );
20 | }
21 |
22 | function AvatarImage({
23 | className,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
32 | );
33 | }
34 |
35 | function AvatarFallback({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | );
49 | }
50 |
51 | export { Avatar, AvatarImage, AvatarFallback };
52 |
--------------------------------------------------------------------------------
/src/components/thread/syntax-highlighter.tsx:
--------------------------------------------------------------------------------
1 | import { PrismAsyncLight as SyntaxHighlighterPrism } from "react-syntax-highlighter";
2 | import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
3 | import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
4 | import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
5 | import { FC } from "react";
6 |
7 | // Register languages you want to support
8 | SyntaxHighlighterPrism.registerLanguage("js", tsx);
9 | SyntaxHighlighterPrism.registerLanguage("jsx", tsx);
10 | SyntaxHighlighterPrism.registerLanguage("ts", tsx);
11 | SyntaxHighlighterPrism.registerLanguage("tsx", tsx);
12 | SyntaxHighlighterPrism.registerLanguage("python", python);
13 |
14 | interface SyntaxHighlighterProps {
15 | children: string;
16 | language: string;
17 | className?: string;
18 | }
19 |
20 | export const SyntaxHighlighter: FC = ({
21 | children,
22 | language,
23 | className,
24 | }) => {
25 | return (
26 |
37 | {children}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Computer Use Agent Generative UI
2 |
3 | A Generative UI web app for interacting with Computer Use Agents (CUA) via the `@langchain/langgraph-cua` prebuilt package.
4 |
5 | 
6 |
7 | ## Quickstart
8 |
9 | The following API keys are required to run this app:
10 |
11 | - [OpenAI API Key](https://platform.openai.com/) - Calling the computer use model
12 | - [Scrapybara API Key](https://scrapybara.com/) - Running the VM
13 |
14 | Once you have both API keys, you can clone the repo:
15 |
16 | ```bash
17 | git clone https://github.com/bracesproul/gen-ui-computer-use.git
18 | ```
19 |
20 | Navigate to the project directory:
21 |
22 | ```bash
23 | cd gen-ui-computer-use
24 | ```
25 |
26 | Copy the `.env.example` file to `.env` and add your API keys:
27 |
28 | ```bash
29 | cp .env.example .env
30 | ```
31 |
32 | Add your OpenAI API key and Scrapybara API key to the `.env` file:
33 |
34 | ```bash
35 | OPENAI_API_KEY=your_openai_api_key
36 | SCrapybara_API_KEY=your_scrapybara_api_key
37 | ```
38 |
39 | Install dependencies:
40 |
41 | ```bash
42 | pnpm install
43 | ```
44 |
45 | Run both the agent and the app:
46 |
47 | ```bash
48 | # Run the web server
49 | pnpm run dev
50 | ```
51 |
52 | In another terminal window:
53 |
54 | ```bash
55 | # Run the agent
56 | pnpm run agent
57 | ```
58 |
59 | The app will be available at `http://localhost:3000`.
60 |
61 | ## License
62 |
63 | [MIT](./LICENSE)
64 |
--------------------------------------------------------------------------------
/src/components/ui/password-input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { Input } from "./input";
7 | import { Button } from "./button";
8 | import { EyeIcon, EyeOffIcon } from "lucide-react";
9 |
10 | export const PasswordInput = React.forwardRef<
11 | HTMLInputElement,
12 | React.ComponentProps<"input">
13 | >(({ className, ...props }, ref) => {
14 | const [showPassword, setShowPassword] = React.useState(false);
15 |
16 | return (
17 |
18 |
24 |
40 |
41 | {/* hides browsers password toggles */}
42 |
50 |
51 | );
52 | });
53 |
54 | PasswordInput.displayName = "PasswordInput";
55 |
--------------------------------------------------------------------------------
/src/agent/ui/instance/instance-view.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { ReactNode } from "react";
3 | import { WindowManagerButtons } from "./window-manager-buttons";
4 |
5 | export function InstanceView({
6 | children,
7 | handleStop,
8 | handlePause,
9 | isStopping,
10 | isStopped,
11 | allDisabled,
12 | handleExpand,
13 | isExpanded,
14 | }: {
15 | children: ReactNode;
16 | handleStop: () => void;
17 | handlePause: () => void;
18 | isStopping: boolean;
19 | isStopped: boolean;
20 | allDisabled: boolean;
21 | handleExpand: () => void;
22 | isExpanded?: boolean;
23 | }) {
24 | return (
25 |
33 |
40 |
41 |
49 |
50 |
{children}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | );
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
25 | );
26 | }
27 |
28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
29 | return (
30 |
35 | );
36 | }
37 |
38 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
39 | return (
40 |
45 | );
46 | }
47 |
48 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
49 | return (
50 |
55 | );
56 | }
57 |
58 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
59 | return (
60 |
65 | );
66 | }
67 |
68 | export {
69 | Card,
70 | CardHeader,
71 | CardFooter,
72 | CardTitle,
73 | CardDescription,
74 | CardContent,
75 | };
76 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | );
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return ;
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
60 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # Run formatting on all PRs
2 |
3 | name: CI
4 |
5 | on:
6 | push:
7 | branches: ["main"]
8 | pull_request:
9 | workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
10 |
11 | # If another push to the same PR or branch happens while this workflow is still running,
12 | # cancel the earlier run in favor of the next run.
13 | #
14 | # There's no point in testing an outdated version of the code. GitHub only allows
15 | # a limited number of job runners to be active at the same time, so it's better to cancel
16 | # pointless jobs early so that more useful jobs can run sooner.
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.ref }}
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | format:
23 | name: Check formatting
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v4
27 | - uses: pnpm/action-setup@v4
28 | with:
29 | version: 10.6.3
30 | - uses: actions/setup-node@v4
31 | with:
32 | node-version: "18.x"
33 | cache: "pnpm"
34 | - name: Install dependencies
35 | run: pnpm install
36 | - name: Check formatting
37 | run: pnpm format:check
38 |
39 | lint:
40 | name: Check linting
41 | runs-on: ubuntu-latest
42 | steps:
43 | - uses: actions/checkout@v4
44 | - uses: pnpm/action-setup@v4
45 | with:
46 | version: 10.6.3
47 | - uses: actions/setup-node@v4
48 | with:
49 | node-version: "18.x"
50 | cache: "pnpm"
51 | - name: Install dependencies
52 | run: pnpm install
53 | - name: Check linting
54 | run: pnpm lint
55 |
56 | readme-spelling:
57 | name: Check README spelling
58 | runs-on: ubuntu-latest
59 | steps:
60 | - uses: actions/checkout@v4
61 | - uses: codespell-project/actions-codespell@v2
62 | with:
63 | ignore_words_file: .codespellignore
64 | path: README.md
65 |
66 | check-spelling:
67 | name: Check code spelling
68 | runs-on: ubuntu-latest
69 | steps:
70 | - uses: actions/checkout@v4
71 | - uses: codespell-project/actions-codespell@v2
72 | with:
73 | ignore_words_file: .codespellignore
74 | path: src
75 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{ts,tsx,js,jsx}",
7 | "./agent/**/*.{ts,tsx,js,jsx}",
8 | ],
9 | theme: {
10 | extend: {
11 | borderRadius: {
12 | lg: "var(--radius)",
13 | md: "calc(var(--radius) - 2px)",
14 | sm: "calc(var(--radius) - 4px)",
15 | },
16 | components: {
17 | ".scrollbar-pretty":
18 | "overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent",
19 | },
20 | colors: {
21 | background: "hsl(var(--background))",
22 | foreground: "hsl(var(--foreground))",
23 | card: {
24 | DEFAULT: "hsl(var(--card))",
25 | foreground: "hsl(var(--card-foreground))",
26 | },
27 | popover: {
28 | DEFAULT: "hsl(var(--popover))",
29 | foreground: "hsl(var(--popover-foreground))",
30 | },
31 | primary: {
32 | DEFAULT: "hsl(var(--primary))",
33 | foreground: "hsl(var(--primary-foreground))",
34 | },
35 | secondary: {
36 | DEFAULT: "hsl(var(--secondary))",
37 | foreground: "hsl(var(--secondary-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | destructive: {
48 | DEFAULT: "hsl(var(--destructive))",
49 | foreground: "hsl(var(--destructive-foreground))",
50 | },
51 | border: "hsl(var(--border))",
52 | input: "hsl(var(--input))",
53 | ring: "hsl(var(--ring))",
54 | chart: {
55 | 1: "hsl(var(--chart-1))",
56 | 2: "hsl(var(--chart-2))",
57 | 3: "hsl(var(--chart-3))",
58 | 4: "hsl(var(--chart-4))",
59 | 5: "hsl(var(--chart-5))",
60 | },
61 | },
62 | },
63 | },
64 | plugins: [require("tailwindcss-animate"), require("tailwind-scrollbar")],
65 | };
66 |
--------------------------------------------------------------------------------
/src/providers/Thread.tsx:
--------------------------------------------------------------------------------
1 | import { validate } from "uuid";
2 | import { Thread } from "@langchain/langgraph-sdk";
3 | import {
4 | createContext,
5 | useContext,
6 | ReactNode,
7 | useCallback,
8 | useState,
9 | Dispatch,
10 | SetStateAction,
11 | useEffect,
12 | } from "react";
13 | import { createClient } from "./client";
14 | import { getItem, USER_ID_KEY } from "@/lib/local-storage";
15 |
16 | interface ThreadContextType {
17 | getThreads: (userId: string) => Promise;
18 | threads: Thread[];
19 | setThreads: Dispatch>;
20 | threadsLoading: boolean;
21 | setThreadsLoading: Dispatch>;
22 | }
23 |
24 | const ThreadContext = createContext(undefined);
25 |
26 | function getThreadSearchMetadata(
27 | assistantId: string,
28 | ): { graph_id: string } | { assistant_id: string } {
29 | if (validate(assistantId)) {
30 | return { assistant_id: assistantId };
31 | } else {
32 | return { graph_id: assistantId };
33 | }
34 | }
35 |
36 | export function ThreadProvider({ children }: { children: ReactNode }) {
37 | const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
38 | const assistantId = "agent";
39 |
40 | const [threads, setThreads] = useState([]);
41 | const [threadsLoading, setThreadsLoading] = useState(false);
42 |
43 | const getThreads = useCallback(
44 | async (userId: string): Promise => {
45 | const client = createClient(apiUrl);
46 |
47 | const threads = await client.threads.search({
48 | metadata: {
49 | ...getThreadSearchMetadata(assistantId),
50 | user_id: userId,
51 | },
52 | limit: 100,
53 | });
54 |
55 | return threads;
56 | },
57 | [apiUrl, assistantId],
58 | );
59 |
60 | const value = {
61 | getThreads,
62 | threads,
63 | setThreads,
64 | threadsLoading,
65 | setThreadsLoading,
66 | };
67 |
68 | return (
69 | {children}
70 | );
71 | }
72 |
73 | export function useThreads() {
74 | const context = useContext(ThreadContext);
75 | if (context === undefined) {
76 | throw new Error("useThreads must be used within a ThreadProvider");
77 | }
78 | return context;
79 | }
80 |
--------------------------------------------------------------------------------
/src/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-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_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",
16 | outline:
17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | brand: "bg-[#2F6868] hover:bg-[#2F6868]/90 border-[#2F6868] text-white",
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 | type ButtonProps = React.ComponentProps<"button"> &
39 | VariantProps & {
40 | asChild?: boolean;
41 | };
42 |
43 | function Button({
44 | className,
45 | variant,
46 | size,
47 | asChild = false,
48 | ...props
49 | }: ButtonProps) {
50 | const Comp = asChild ? Slot : "button";
51 |
52 | return (
53 |
58 | );
59 | }
60 |
61 | export { Button, buttonVariants, type ButtonProps };
62 |
--------------------------------------------------------------------------------
/src/agent/ui/computer-use-tool-output.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import "./styles.css";
4 | import { ChevronDown, ChevronUp } from "lucide-react";
5 | import { useState } from "react";
6 |
7 | interface ComputerUseToolOutputProps {
8 | toolCallId: string;
9 | /**
10 | * The base64 encoded screenshot of the computer
11 | */
12 | screenshot: string;
13 | }
14 |
15 | export function ComputerUseToolOutput(props: ComputerUseToolOutputProps) {
16 | const { screenshot, toolCallId } = props;
17 | const [isExpanded, setIsExpanded] = useState(false);
18 |
19 | return (
20 |
21 | {/* Desktop and mobile header row */}
22 |
23 | {/* Mobile layout: Title and toggle button in first row */}
24 |
25 |
Computer Output
26 |
39 |
40 |
41 | {/* Desktop layout: Title and ID on left, button on right */}
42 |
43 |
Computer Output
44 |
{toolCallId}
45 |
46 |
47 |
60 |
61 | {/* Mobile only: Tool call ID in second row */}
62 |
63 | {toolCallId}
64 |
65 |
66 |
67 | {/* Screenshot container */}
68 | {isExpanded && (
69 |
70 |

75 |
76 | )}
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gen-ui-computer-use",
3 | "readme": "https://github.com/bracesproul/gen-ui-computer-use/blob/main/README.md",
4 | "homepage": "https://gen-ui-computer-use.vercel.app",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/bracesproul/gen-ui-computer-use.git"
8 | },
9 | "private": true,
10 | "version": "0.0.0",
11 | "type": "module",
12 | "scripts": {
13 | "dev": "next dev",
14 | "agent": "npx @langchain/langgraph-cli dev",
15 | "build": "next build",
16 | "start": "next start",
17 | "lint": "next lint",
18 | "format": "prettier --write .",
19 | "format:check": "prettier --check ."
20 | },
21 | "dependencies": {
22 | "@langchain/core": "^0.3.43",
23 | "@langchain/langgraph": "^0.2.60",
24 | "@langchain/langgraph-api": "^0.0.19",
25 | "@langchain/langgraph-cli": "^0.0.19",
26 | "@langchain/langgraph-cua": "^0.0.5",
27 | "@langchain/langgraph-sdk": "^0.0.61",
28 | "@langchain/openai": "^0.5.1",
29 | "@radix-ui/react-alert-dialog": "^1.1.6",
30 | "@radix-ui/react-avatar": "^1.1.3",
31 | "@radix-ui/react-dialog": "^1.1.6",
32 | "@radix-ui/react-label": "^2.1.2",
33 | "@radix-ui/react-separator": "^1.1.2",
34 | "@radix-ui/react-slot": "^1.1.2",
35 | "@radix-ui/react-switch": "^1.1.3",
36 | "@radix-ui/react-tooltip": "^1.1.8",
37 | "@supabase/supabase-js": "^2.49.4",
38 | "class-variance-authority": "^0.7.1",
39 | "clsx": "^2.1.1",
40 | "date-fns": "^4.1.0",
41 | "esbuild": "^0.25.0",
42 | "esbuild-plugin-tailwindcss": "^2.0.1",
43 | "framer-motion": "^12.4.9",
44 | "katex": "^0.16.21",
45 | "langgraph-nextjs-api-passthrough": "^0.0.4",
46 | "lodash": "^4.17.21",
47 | "lucide-react": "^0.476.0",
48 | "next-themes": "^0.4.4",
49 | "nuqs": "^2.4.1",
50 | "prettier": "^3.5.2",
51 | "react": "^19.0.0",
52 | "react-dom": "^19.0.0",
53 | "react-markdown": "^10.0.1",
54 | "react-syntax-highlighter": "^15.5.0",
55 | "recharts": "^2.15.1",
56 | "rehype-katex": "^7.0.1",
57 | "remark-gfm": "^4.0.1",
58 | "remark-math": "^6.0.0",
59 | "scrapybara": "^2.4.4",
60 | "sonner": "^2.0.1",
61 | "tailwind-merge": "^3.0.2",
62 | "tailwindcss-animate": "^1.0.7",
63 | "use-stick-to-bottom": "^1.0.46",
64 | "uuid": "^11.0.5",
65 | "zod": "^3.24.2"
66 | },
67 | "devDependencies": {
68 | "@eslint/js": "^9.19.0",
69 | "@tailwindcss/postcss": "^4.0.13",
70 | "@types/lodash": "^4.17.16",
71 | "@types/node": "^22.13.5",
72 | "@types/react": "^19.0.8",
73 | "@types/react-dom": "^19.0.3",
74 | "@types/react-syntax-highlighter": "^15.5.13",
75 | "autoprefixer": "^10.4.20",
76 | "dotenv": "^16.4.7",
77 | "eslint": "^9.19.0",
78 | "eslint-config-next": "15.2.2",
79 | "eslint-plugin-react-hooks": "^5.0.0",
80 | "eslint-plugin-react-refresh": "^0.4.18",
81 | "globals": "^15.14.0",
82 | "next": "^15.2.4",
83 | "openai": "^4.87.3",
84 | "postcss": "^8.5.3",
85 | "tailwind-scrollbar": "^4.0.1",
86 | "tailwindcss": "^4.0.13",
87 | "turbo": "latest",
88 | "typescript": "~5.7.2",
89 | "typescript-eslint": "^8.22.0"
90 | },
91 | "overrides": {
92 | "react-is": "^19.0.0-rc-69d4b800-20241021"
93 | },
94 | "packageManager": "pnpm@10.6.3"
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/thread/messages/human.tsx:
--------------------------------------------------------------------------------
1 | import { useStreamContext } from "@/providers/Stream";
2 | import { Message } from "@langchain/langgraph-sdk";
3 | import { useState } from "react";
4 | import { getContentString } from "../utils";
5 | import { cn } from "@/lib/utils";
6 | import { Textarea } from "@/components/ui/textarea";
7 | import { BranchSwitcher, CommandBar } from "./shared";
8 |
9 | function EditableContent({
10 | value,
11 | setValue,
12 | onSubmit,
13 | }: {
14 | value: string;
15 | setValue: React.Dispatch>;
16 | onSubmit: () => void;
17 | }) {
18 | const handleKeyDown = (e: React.KeyboardEvent) => {
19 | if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
20 | e.preventDefault();
21 | onSubmit();
22 | }
23 | };
24 |
25 | return (
26 |