├── .env.example ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ ├── check.yml │ ├── format.yml │ └── lint.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── app ├── components │ ├── Agent.tsx │ ├── AgentAssets.tsx │ ├── AgentBalance.tsx │ ├── AgentProfile.tsx │ ├── Chat.tsx │ ├── ChatInput.tsx │ ├── Footer.tsx │ ├── LanguageSelector.tsx │ ├── Navbar.tsx │ ├── Stream.tsx │ ├── StreamItem.tsx │ └── TimeDisplay.tsx ├── config.ts ├── constants.ts ├── globals.css ├── hooks │ ├── useChat.ts │ ├── useGetNFTs.ts │ └── useGetTokens.ts ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png ├── layout.tsx ├── page.tsx ├── providers.tsx ├── svg │ ├── ArrowSvg.tsx │ ├── ChatSvg.tsx │ ├── CopySvg.tsx │ ├── Image.tsx │ ├── NftSvg.tsx │ ├── OnchainKit.tsx │ ├── RequestSvg.tsx │ ├── SendSvg.tsx │ ├── StreamSvg.tsx │ ├── SwapSvg.tsx │ ├── TokenSvg.tsx │ └── WalletSvg.tsx ├── translations.ts ├── types.ts └── utils.tsx ├── biome.json ├── bun.lockb ├── next.config.js ├── package.json ├── postcss.config.mjs ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Template Build 2 | on: 3 | push: 4 | branches: ['master'] 5 | pull_request: 6 | branches: ['master'] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Bun 19 | uses: oven-sh/setup-bun@v2 20 | 21 | - name: Template Install dependencies 22 | run: bun install 23 | 24 | - name: Template Test Build 25 | # When fails, please check your build 26 | run: | 27 | bun run build -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Template Check 2 | on: 3 | push: 4 | branches: ["master"] 5 | pull_request: 6 | branches: ["master"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Bun 19 | uses: oven-sh/setup-bun@v2 20 | 21 | - name: Template Install dependencies 22 | run: bun install 23 | 24 | - name: Template Check 25 | run: bun run ci:check -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Template Format 2 | on: 3 | push: 4 | branches: ["master"] 5 | pull_request: 6 | branches: ["master"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Bun 19 | uses: oven-sh/setup-bun@v2 20 | 21 | - name: Template Install dependencies 22 | run: bun install 23 | 24 | - name: Template Format 25 | run: bun run ci:format -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Template Lint 2 | on: 3 | push: 4 | branches: ["master"] 5 | pull_request: 6 | branches: ["master"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Bun 19 | uses: oven-sh/setup-bun@v2 20 | 21 | - name: Template Install dependencies 22 | run: bun install 23 | 24 | - name: Template Lint 25 | run: bun run ci:lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # vercel 26 | .vercel 27 | 28 | # typescript 29 | *.tsbuildinfo 30 | next-env.d.ts 31 | 32 | .env 33 | 34 | # python 35 | /api/__pycache__/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | - [Bun](https://bun.sh) for package management 4 | 5 | You'll also need to add a `.env` file with the following variable: 6 | 7 | ```bash 8 | NEXT_PUBLIC_API_URL= # The base URL for API requests. This must be set to the endpoint of your backend service. 9 | ``` 10 | 11 | ## Running locally 12 | 13 | - Install dependencies 14 | ```bash 15 | bun i 16 | ``` 17 | 18 | - Start the local development server 19 | ```bash 20 | bun dev 21 | ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache-2.0 License 2 | 3 | Copyright 2024 Coinbase 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Onchain Agent Demo 2 | 3 | ![Token-creation](https://github.com/user-attachments/assets/016c26cd-c599-4f7c-bafd-c8090069b53e) 4 | 5 | 6 | A web app that enables onchain interactions through a conversational UI using AgentKit, a collaboration between [CDP SDK](https://docs.cdp.coinbase.com/) and [OnchainKit](https://onchainkit.xyz). 7 | 8 | ## Overview 9 | 10 | This project features a Next.js frontend designed to work seamlessly with [CDP's AgentKit backend](https://github.com/coinbase/onchain-agent-demo-backend). Together, they enable the creation of an AI agent capable of performing onchain operations on Base. The agent uses GPT-4 for natural language understanding and AgentKit for onchain interactions. 11 | 12 | ## Key Features 13 | 14 | - **AI-Powered Chat Interface**: Interactive chat interface for natural language interactions onchain 15 | - **Onchain Operations**: Ability to perform various blockchain operations through Agentkit: 16 | - Deploy and interact with ERC-20 tokens 17 | - Create and manage NFTs 18 | - Check wallet balances 19 | - Request funds from faucet 20 | - **Real-time Updates**: Server-Sent Events (SSE) for streaming responses 21 | - **Responsive Design**: Modern UI built with Tailwind CSS 22 | - **Wallet Integration**: Secure wallet management through CDP Agentkit 23 | 24 | ## Tech Stack 25 | 26 | - **Frontend**: Next.js 14, React, Tailwind CSS 27 | - **Development**: TypeScript, Biome for formatting/linting 28 | 29 | ## Prerequisites 30 | 31 | - [Bun](https://bun.sh) for package management 32 | 33 | ## Environment Setup 34 | 35 | Create a `.env.local` file with the following variables: 36 | 37 | ```bash 38 | NEXT_PUBLIC_API_URL= # The base URL for API requests. This must be set to the endpoint of your backend service. 39 | ``` 40 | 41 | ## Installation 42 | 43 | 1. Install dependencies: 44 | ```bash 45 | bun i 46 | ``` 47 | 48 | 2. Start the development server: 49 | ```bash 50 | bun dev 51 | ``` 52 | 53 | ## Development 54 | 55 | - Format code: `bun run format` 56 | - Lint code: `bun run lint` 57 | - Run CI checks: `bun run ci:check` 58 | 59 | ## Deploying to Replit 60 | 61 | - [Frontend Template](https://replit.com/@alissacrane1/onchain-agent-demo-frontend?v=1) 62 | - [Backend Template](https://replit.com/@alissacrane1/onchain-agent-demo-backend?v=1) 63 | 64 | Steps: 65 | - Sign up for a Replit account, or login to your existing one. 66 | - Navigate to the template links, and click `Use Template` on the top right hand side. 67 | - Under `Secrets` in `Workspace Features`, add the environment variables below. 68 | - Tip: You can click `Edit as JSON` and copy the values below in. 69 | - Click `Deploy` in the top right. 70 | - Tip: Deploy your backend first, as you'll need the deployment URL to set as `NEXT_PUBLIC_API_URL` in the frontend. 71 | - Tip: You can click `Run` to test if the applications run properly before deploying. 72 | 73 | **Backend Secrets** 74 | ``` 75 | { 76 | "CDP_API_KEY_NAME": "get this from https://portal.cdp.coinbase.com/projects/api-keys", 77 | "CDP_API_KEY_PRIVATE_KEY": "get this from https://portal.cdp.coinbase.com/projects/api-keys", 78 | "OPENAI_API_KEY": "get this from https://platform.openai.com/api-keys", 79 | "NETWORK_ID": "base-sepolia" 80 | } 81 | ``` 82 | 83 | **Important: Replit resets the SQLite template on every deployment, before sending funds to your agent or using it on Mainnet be sure to read [Agent Wallet](https://github.com/coinbase/onchain-agent-demo-backend?tab=readme-ov-file#agent-wallet) and save your wallet ID and seed in a safe place.** 84 | 85 | **Frontend Secrets** 86 | ``` 87 | { 88 | "NEXT_PUBLIC_API_URL": "your backend deployment URL here" 89 | } 90 | ``` 91 | 92 | Note: you'll need to include the scheme (`https://`) in `NEXT_PUBLIC_API_URL`. 93 | 94 | ## License 95 | 96 | See [LICENSE.md](LICENSE.md) for details. 97 | 98 | ## Contributing 99 | 100 | See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project. 101 | 102 | Special shoutout to [Shu Ding](https://x.com/shuding) for his amazing generative UI for the NFT art. 103 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The Coinbase team takes security seriously. Please do not file a public ticket discussing a potential vulnerability. 4 | 5 | Please report your findings through our [HackerOne][1] program. 6 | 7 | [1]: https://hackerone.com/coinbase -------------------------------------------------------------------------------- /app/components/Agent.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import type { Address } from 'viem'; 3 | import useGetNFTs from '../hooks/useGetNFTs'; 4 | import useGetTokens from '../hooks/useGetTokens'; 5 | import AgentAssets from './AgentAssets'; 6 | import AgentProfile from './AgentProfile'; 7 | import Chat from './Chat'; 8 | import Navbar from './Navbar'; 9 | import Stream from './Stream'; 10 | 11 | export default function Agent() { 12 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 13 | const [isMobileChatOpen, setIsMobileChatOpen] = useState(false); 14 | 15 | const [nfts, setNFTs] = useState([]); 16 | const [tokens, setTokens] = useState([]); 17 | 18 | const { getTokens } = useGetTokens({ onSuccess: setTokens }); 19 | const { getNFTs } = useGetNFTs({ onSuccess: setNFTs }); 20 | 21 | return ( 22 |
23 | 29 | 30 |
31 |
37 | 38 | 44 |
45 | 46 |
47 | 48 | 49 |
50 | 51 |
57 | 58 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/components/AgentAssets.tsx: -------------------------------------------------------------------------------- 1 | import { NFTMintCard } from '@coinbase/onchainkit/nft'; 2 | import { NFTCollectionTitle } from '@coinbase/onchainkit/nft/mint'; 3 | import { NFTMedia } from '@coinbase/onchainkit/nft/view'; 4 | import { type Token, TokenRow } from '@coinbase/onchainkit/token'; 5 | import { useCallback, useEffect, useMemo, useState } from 'react'; 6 | import { type Address, erc721Abi } from 'viem'; 7 | import { useContractRead, useToken } from 'wagmi'; 8 | 9 | type AgentAssetProps = { 10 | tokenAddress: Address; 11 | index?: number; 12 | }; 13 | 14 | function AgentToken({ tokenAddress }: AgentAssetProps) { 15 | const { data } = useToken({ address: tokenAddress, chainId: 84532 }); 16 | const token: Token = { 17 | address: tokenAddress, 18 | chainId: 84532, 19 | decimals: data?.decimals || 0, 20 | name: data?.name || '', 21 | symbol: data?.symbol || '', 22 | image: '', 23 | }; 24 | 25 | return ; 26 | } 27 | 28 | function AgentNFT({ index = 0, tokenAddress }: AgentAssetProps) { 29 | const { data: name } = useContractRead({ 30 | address: tokenAddress, 31 | abi: erc721Abi, 32 | functionName: 'name', 33 | chainId: 84532, 34 | }); 35 | 36 | const nftData = useMemo(() => { 37 | return { 38 | name, 39 | imageUrl: `https://raw.githubusercontent.com/coinbase/onchain-agent-demo/master/app/images/${(index % 8) + 1}.png`, 40 | }; 41 | }, [name, index]); 42 | 43 | if (!name) { 44 | return null; 45 | } 46 | 47 | return ( 48 | nftData} 52 | > 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | type AgentAssetsProps = { 60 | getTokens: () => void; 61 | getNFTs: () => void; 62 | nfts: Address[]; 63 | tokens: Address[]; 64 | }; 65 | 66 | export default function AgentAssets({ 67 | getTokens, 68 | getNFTs, 69 | tokens, 70 | nfts, 71 | }: AgentAssetsProps) { 72 | const [tab, setTab] = useState('tokens'); 73 | 74 | const handleTabChange = useCallback((tab: string) => { 75 | return () => setTab(tab); 76 | }, []); 77 | 78 | useEffect(() => { 79 | getNFTs(); 80 | getTokens(); 81 | }, [getNFTs, getTokens]); 82 | 83 | return ( 84 |
85 |

My creations

86 |
87 |
88 | 97 | 106 |
107 | 108 | {tab === 'tokens' && 109 | tokens && 110 | tokens?.map((token) => ( 111 | 112 | ))} 113 | 114 | {tab === 'nfts' && nfts && ( 115 |
116 | {nfts?.map((nft, index) => ( 117 | 118 | ))} 119 |
120 | )} 121 |
122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /app/components/AgentBalance.tsx: -------------------------------------------------------------------------------- 1 | import { useBalance } from 'wagmi'; 2 | import { AGENT_WALLET_ADDRESS } from '../constants'; 3 | 4 | export default function AgentBalance() { 5 | const { data } = useBalance({ 6 | address: AGENT_WALLET_ADDRESS, 7 | query: { refetchInterval: 5000 }, 8 | }); 9 | 10 | if (!data) { 11 | return null; 12 | } 13 | 14 | return ( 15 |
16 | 17 | {`${Number.parseFloat(data?.formatted || '').toFixed(6)} ETH`} 18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/AgentProfile.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2 | import { AGENT_NAME, AGENT_WALLET_ADDRESS } from '../constants'; 3 | import AgentBalance from './AgentBalance'; 4 | 5 | export default function AgentProfile() { 6 | const [eyePosition, setEyePosition] = useState({ x: 50, y: 50 }); 7 | const [showToast, setShowToast] = useState(false); 8 | const avatarRef = useRef(null); 9 | 10 | const copyToClipboard = useCallback(() => { 11 | navigator.clipboard 12 | .writeText(AGENT_WALLET_ADDRESS) 13 | .then(() => { 14 | setShowToast(true); 15 | setTimeout(() => setShowToast(false), 2000); 16 | }) 17 | .catch((err) => { 18 | console.error('Failed to copy wallet address: ', err); 19 | }); 20 | }, []); 21 | 22 | useEffect(() => { 23 | const handleMouseMove = (event: MouseEvent) => { 24 | if (avatarRef.current) { 25 | const avatarRect = avatarRef.current.getBoundingClientRect(); 26 | const avatarCenterX = avatarRect.left + avatarRect.width / 2; 27 | const avatarCenterY = avatarRect.top + avatarRect.height / 2; 28 | 29 | const dx = event.clientX - avatarCenterX; 30 | const dy = event.clientY - avatarCenterY; 31 | const maxDistance = Math.max(window.innerWidth, window.innerHeight) / 2; 32 | 33 | const normalizedX = Math.min( 34 | Math.max((dx / maxDistance) * 30 + 50, 20), 35 | 80, 36 | ); 37 | const normalizedY = Math.min( 38 | Math.max((dy / maxDistance) * 30 + 50, 20), 39 | 80, 40 | ); 41 | 42 | setEyePosition({ x: normalizedX, y: normalizedY }); 43 | } 44 | }; 45 | 46 | window.addEventListener('mousemove', handleMouseMove); 47 | return () => window.removeEventListener('mousemove', handleMouseMove); 48 | }, []); 49 | 50 | const formattedAddress = useMemo(() => { 51 | return `${AGENT_WALLET_ADDRESS.slice(0, 6)}...${AGENT_WALLET_ADDRESS.slice( 52 | -4, 53 | )}`; 54 | }, []); 55 | 56 | return ( 57 |
58 |
59 |
60 | 69 | 70 | 76 | 77 | 78 |
79 |
80 |

{AGENT_NAME}

81 | 88 | {showToast && ( 89 |
90 | Copied 91 |
92 | )} 93 |
94 |
95 | 96 |
97 |
98 |
99 | 100 |

101 | I observe, imagine, and create onchain. 102 |

103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /app/components/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@coinbase/onchainkit/theme'; 2 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 3 | import useChat from '../hooks/useChat'; 4 | import type { AgentMessage, StreamEntry } from '../types'; 5 | import { generateUUID, markdownToPlainText } from '../utils'; 6 | import ChatInput from './ChatInput'; 7 | import StreamItem from './StreamItem'; 8 | 9 | type ChatProps = { 10 | className?: string; 11 | getNFTs: () => void; 12 | getTokens: () => void; 13 | }; 14 | 15 | export default function Chat({ className, getNFTs, getTokens }: ChatProps) { 16 | const [userInput, setUserInput] = useState(''); 17 | const [streamEntries, setStreamEntries] = useState([]); 18 | const conversationId = useMemo(() => { 19 | return generateUUID(); 20 | }, []); 21 | 22 | const [shouldRefetchNFTs, setShouldRefetchNFTs] = useState(false); 23 | const [shouldRefetchTokens, setShouldRefetchTokens] = useState(false); 24 | 25 | useEffect(() => { 26 | if (shouldRefetchNFTs) { 27 | getNFTs(); 28 | setShouldRefetchNFTs(false); 29 | } 30 | }, [getNFTs, shouldRefetchNFTs]); 31 | 32 | useEffect(() => { 33 | if (shouldRefetchTokens) { 34 | getTokens(); 35 | setShouldRefetchTokens(false); 36 | } 37 | }, [getTokens, shouldRefetchTokens]); 38 | 39 | const bottomRef = useRef(null); 40 | 41 | const handleSuccess = useCallback((messages: AgentMessage[]) => { 42 | const functions = 43 | messages?.find((msg) => msg.event === 'tools')?.functions || []; 44 | if (functions?.includes('deploy_nft')) { 45 | setShouldRefetchNFTs(true); 46 | } 47 | if (functions?.includes('deploy_token')) { 48 | setShouldRefetchTokens(true); 49 | } 50 | 51 | let message = messages.find((res) => res.event === 'agent'); 52 | if (!message) { 53 | message = messages.find((res) => res.event === 'tools'); 54 | } 55 | if (!message) { 56 | message = messages.find((res) => res.event === 'error'); 57 | } 58 | const streamEntry: StreamEntry = { 59 | timestamp: new Date(), 60 | content: markdownToPlainText(message?.data || ''), 61 | type: 'agent', 62 | }; 63 | setStreamEntries((prev) => [...prev, streamEntry]); 64 | }, []); 65 | 66 | const { postChat, isLoading } = useChat({ 67 | onSuccess: handleSuccess, 68 | conversationId, 69 | }); 70 | 71 | const handleSubmit = useCallback( 72 | async (e: React.FormEvent) => { 73 | e.preventDefault(); 74 | if (!userInput.trim()) { 75 | return; 76 | } 77 | 78 | setUserInput(''); 79 | 80 | const userMessage: StreamEntry = { 81 | timestamp: new Date(), 82 | type: 'user', 83 | content: userInput.trim(), 84 | }; 85 | 86 | setStreamEntries((prev) => [...prev, userMessage]); 87 | 88 | postChat(userInput); 89 | }, 90 | [postChat, userInput], 91 | ); 92 | 93 | const handleKeyPress = useCallback( 94 | (e: React.KeyboardEvent) => { 95 | if (e.key === 'Enter' && !e.shiftKey) { 96 | e.preventDefault(); 97 | handleSubmit(e); 98 | } 99 | }, 100 | [handleSubmit], 101 | ); 102 | 103 | // biome-ignore lint/correctness/useExhaustiveDependencies: Dependency is required 104 | useEffect(() => { 105 | // scrolls to the bottom of the chat when messages change 106 | bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); 107 | }, [streamEntries]); 108 | 109 | return ( 110 |
116 |
117 |

What's on your mind...

118 |
119 | {streamEntries.map((entry, index) => ( 120 | 124 | ))} 125 |
126 | 127 |
128 |
129 | 130 | 137 |
138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /app/components/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | import { type ChangeEvent, useCallback } from 'react'; 2 | import SendSvg from '../svg/SendSvg'; 3 | 4 | type PremadeChatInputProps = { 5 | text: string; 6 | setUserInput: (input: string) => void; 7 | }; 8 | 9 | function PremadeChatInput({ text, setUserInput }: PremadeChatInputProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | 21 | export type ChatInputProps = { 22 | handleSubmit: (e: React.FormEvent) => void; 23 | userInput: string; 24 | setUserInput: (input: string) => void; 25 | handleKeyPress: (e: React.KeyboardEvent) => void; 26 | disabled?: boolean; 27 | }; 28 | 29 | export default function ChatInput({ 30 | handleSubmit, 31 | userInput, 32 | setUserInput, 33 | handleKeyPress, 34 | disabled = false, 35 | }: ChatInputProps) { 36 | const handleInputChange = useCallback( 37 | // TODO: sanitize 38 | (e: ChangeEvent) => { 39 | setUserInput(e.target.value); 40 | }, 41 | [setUserInput], 42 | ); 43 | 44 | return ( 45 |
49 |
50 |
51 |