├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── near-logo.svg ├── near.svg ├── next.svg └── vercel.svg └── src ├── components └── navigation.js ├── config.js ├── lib └── near-ai-api.js ├── pages ├── _app.js └── index.js ├── styles ├── app.module.css └── globals.css ├── utils └── utils.js └── wallets └── web3modal.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | .yarn/install-state.gz 6 | 7 | # testing 8 | /coverage 9 | 10 | # next.js 11 | /.next/ 12 | /out/ 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env*.local 28 | .env 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # IDE 38 | .idea/ 39 | .vscode/ 40 | *.swp 41 | *.swo 42 | 43 | # Logs 44 | logs 45 | *.log 46 | 47 | # Cache 48 | .cache/ 49 | .npm/ 50 | 51 | # Personal workspace files 52 | .workspace/ 53 | *.workspace 54 | 55 | # Database files 56 | *.sqlite 57 | *.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More about NEAR 24 | 25 | To learn more about NEAR, take a look at the following resources: 26 | 27 | - [NEAR Documentation](https://docs.near.org) - learn about NEAR. 28 | - [Frontend Docs](https://docs.near.org/build/web3-apps/quickstart) - learn about this example. 29 | 30 | You can check out [the NEAR repository](https://github.com/near) - your feedback and contributions are welcome! 31 | 32 | ## Learn More about Next.js 33 | 34 | To learn more about Next.js, take a look at the following resources: 35 | 36 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 37 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 38 | 39 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 40 | 41 | ## Deploy on Vercel 42 | 43 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 44 | 45 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 46 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | transpilePackages: ['reown'], 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "near-ai-web-starter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=18" 7 | }, 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "@near-wallet-selector/bitte-wallet": "^9.0.2", 16 | "@near-wallet-selector/core": "^9.0.2", 17 | "@near-wallet-selector/ethereum-wallets": "^9.0.2", 18 | "@near-wallet-selector/here-wallet": "^9.0.2", 19 | "@near-wallet-selector/hot-wallet": "^9.0.2", 20 | "@near-wallet-selector/ledger": "^9.0.2", 21 | "@near-wallet-selector/meteor-wallet": "^9.0.2", 22 | "@near-wallet-selector/meteor-wallet-app": "^9.0.2", 23 | "@near-wallet-selector/modal-ui": "^9.0.2", 24 | "@near-wallet-selector/my-near-wallet": "^9.0.2", 25 | "@near-wallet-selector/near-mobile-wallet": "^9.0.2", 26 | "@near-wallet-selector/react-hook": "^9.0.2", 27 | "@near-wallet-selector/sender": "^9.0.2", 28 | "@near-wallet-selector/welldone-wallet": "^9.0.2", 29 | "@reown/appkit": "^1.7.7", 30 | "@reown/appkit-adapter-wagmi": "^1.7.7", 31 | "@wagmi/core": "^2.17.2", 32 | "bootstrap": "^5", 33 | "bootstrap-icons": "^1.11.3", 34 | "near-api-js": "^5.0.0", 35 | "next": "^15", 36 | "react": "^18", 37 | "react-dom": "^18", 38 | "viem": "^2.30.5" 39 | }, 40 | "devDependencies": { 41 | "encoding": "^0.1.13", 42 | "eslint": "^9", 43 | "eslint-config-next": "^15", 44 | "pino-pretty": "^11.2.2" 45 | } 46 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakhar728/near-ai-web-starter/167f7af88fb1289f4c19350fc558d2a908e635ee/public/favicon.ico -------------------------------------------------------------------------------- /public/near-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 34 | 38 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/near.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/navigation.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { useEffect, useState } from 'react'; 4 | import { useWalletSelector } from '@near-wallet-selector/react-hook'; 5 | 6 | import NearLogo from '/public/near-logo.svg'; 7 | 8 | export const Navigation = () => { 9 | const { signedAccountId, signIn, signOut } = useWalletSelector(); 10 | const [action, setAction] = useState(() => { }); 11 | const [label, setLabel] = useState('Loading...'); 12 | 13 | useEffect(() => { 14 | if (signedAccountId) { 15 | setAction(() => signOut); 16 | setLabel(`Logout ${signedAccountId}`); 17 | } else { 18 | setAction(() => signIn); 19 | setLabel("Login"); 20 | } 21 | }, [signedAccountId]); 22 | 23 | return ( 24 | 34 | ); 35 | }; -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const contractPerNetwork = { 2 | mainnet: 'hello.near-examples.near', 3 | testnet: 'hello.near-examples.testnet', 4 | }; 5 | 6 | export const NetworkId = 'mainnet'; 7 | export const HelloNearContract = contractPerNetwork[NetworkId]; -------------------------------------------------------------------------------- /src/lib/near-ai-api.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = "https://api.near.ai"; 2 | 3 | /** 4 | * @typedef {Object} NearAuthData 5 | * @property {any} message 6 | * @property {String} nonce 7 | * @property {string} recipient 8 | * @property {string} callback_url 9 | * @property {string} signature 10 | * @property {string} account_id 11 | * @property {string} public_key 12 | */ 13 | 14 | /** 15 | * Creates a new thread 16 | * @param {NearAuthData|null} auth - Authentication data 17 | * @returns {Promise} The newly created thread 18 | */ 19 | export const createThread = async (auth) => { 20 | const URL = `${BASE_URL}/v1/threads`; 21 | 22 | const headers = { 23 | Accept: "application/json", 24 | Authorization: `Bearer ${JSON.stringify(auth)}`, 25 | "Content-Type": "application/json", // Specify content type 26 | }; 27 | 28 | const body = { 29 | messages: [ 30 | { 31 | content: "string", 32 | role: "user", 33 | metadata: {}, 34 | }, 35 | ], 36 | }; 37 | 38 | const newThread = await ( 39 | await fetch(URL, { 40 | method: "POST", 41 | headers, 42 | body: JSON.stringify(body), 43 | }) 44 | ).json(); 45 | 46 | return newThread; 47 | }; 48 | 49 | /** 50 | * Runs an agent on a thread with a message 51 | * @param {NearAuthData} auth - Authentication data 52 | * @param {string} agent - The agent ID 53 | * @param {string} thread - The thread ID 54 | * @param {string} message - The message to send 55 | * @returns {Promise} The result of running the agent 56 | */ 57 | export const runAgent = async (auth, agent, thread, message) => { 58 | const URL = `${BASE_URL}/v1/agent/runs`; 59 | 60 | const headers = { 61 | Accept: "application/json", 62 | Authorization: `Bearer ${JSON.stringify(auth)}`, 63 | "Content-Type": "application/json", // Specify content type 64 | }; 65 | 66 | const body = { 67 | agent_id: agent, 68 | thread_id: thread, 69 | new_message: message, 70 | max_iterations: 1, 71 | record_run: true, 72 | tool_resources: {}, 73 | user_env_vars: {}, 74 | }; 75 | 76 | let agentThread; 77 | 78 | try { 79 | agentThread = await fetch(URL, { 80 | method: "POST", 81 | headers, 82 | body: JSON.stringify(body), 83 | }); 84 | } catch (error) { 85 | console.log(error); 86 | } 87 | 88 | return (await agentThread.json()); 89 | }; 90 | 91 | /** 92 | * Fetches the current state of a thread 93 | * @param {NearAuthData} auth - Authentication data 94 | * @param {string} thread - The thread ID 95 | * @returns {Promise} The messages in the thread 96 | */ 97 | export const fetchThreadState = async (auth, thread) => { 98 | const URL = `${BASE_URL}/v1/threads/${thread}/messages?order=desc`; 99 | 100 | const headers = { 101 | Accept: "application/json", 102 | Authorization: `Bearer ${JSON.stringify(auth)}`, 103 | "Content-Type": "application/json", // Specify content type 104 | }; 105 | 106 | let messages; 107 | 108 | try { 109 | messages = await fetch(URL, { 110 | method: "GET", 111 | headers, 112 | }); 113 | } catch (error) { 114 | console.log(error); 115 | } 116 | 117 | return (await messages.json()); 118 | }; -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import { Navigation } from '@/components/navigation'; 3 | 4 | import '@near-wallet-selector/modal-ui/styles.css'; 5 | import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; 6 | import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet'; 7 | import { setupMeteorWalletApp } from '@near-wallet-selector/meteor-wallet-app'; 8 | import { setupBitteWallet } from '@near-wallet-selector/bitte-wallet'; 9 | import { setupEthereumWallets } from '@near-wallet-selector/ethereum-wallets'; 10 | import { setupHotWallet } from '@near-wallet-selector/hot-wallet'; 11 | import { setupLedger } from '@near-wallet-selector/ledger'; 12 | import { setupSender } from '@near-wallet-selector/sender'; 13 | import { setupHereWallet } from '@near-wallet-selector/here-wallet'; 14 | import { setupNearMobileWallet } from '@near-wallet-selector/near-mobile-wallet'; 15 | import { setupWelldoneWallet } from '@near-wallet-selector/welldone-wallet'; 16 | import { HelloNearContract, NetworkId } from '@/config'; 17 | import { WalletSelectorProvider } from '@near-wallet-selector/react-hook'; 18 | import { wagmiAdapter, web3Modal } from '@/wallets/web3modal'; 19 | 20 | const walletSelectorConfig = { 21 | network: NetworkId, 22 | // createAccessKeyFor: HelloNearContract, 23 | modules: [ 24 | setupMeteorWallet(), 25 | setupEthereumWallets({ wagmiConfig: wagmiAdapter.wagmiConfig, web3Modal }), 26 | setupBitteWallet(), 27 | setupMeteorWalletApp({ contractId: HelloNearContract }), 28 | setupHotWallet(), 29 | setupLedger(), 30 | setupSender(), 31 | setupHereWallet(), 32 | setupNearMobileWallet(), 33 | setupWelldoneWallet(), 34 | setupMyNearWallet(), 35 | ], 36 | } 37 | 38 | export default function MyApp({ Component, pageProps }) { 39 | return ( 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import { useWalletSelector } from "@near-wallet-selector/react-hook"; 3 | 4 | import styles from "@/styles/app.module.css"; 5 | import { createThread, fetchThreadState, runAgent } from "@/lib/near-ai-api"; 6 | 7 | export default function AgentChat() { 8 | const { signedAccountId, signMessage } = useWalletSelector(); 9 | 10 | const [agentId, setAgentId] = useState(""); 11 | const [threadId, setThreadId] = useState(null); 12 | const [message, setMessage] = useState(""); 13 | const [messages, setMessages] = useState([]); 14 | const [loading, setLoading] = useState(false); 15 | const [error, setError] = useState(null); 16 | const [nearSignatureAuth, setNearSignatureAuth] = useState(null); 17 | 18 | const messagesEndRef = useRef(null); 19 | 20 | // Scroll to bottom when messages update 21 | useEffect(() => { 22 | if (messagesEndRef.current) { 23 | messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); 24 | } 25 | }, [messages]); 26 | 27 | // Authenticate using signMessage 28 | useEffect(() => { 29 | const authenticate = async () => { 30 | if (!signedAccountId || !signMessage) return; 31 | 32 | // Check local storage first 33 | const stored = localStorage.getItem("NearAIAuthObject"); 34 | if (stored) { 35 | try { 36 | const parsed = JSON.parse(stored); 37 | if (parsed.account_id === signedAccountId) { 38 | setNearSignatureAuth(parsed); 39 | return; 40 | } 41 | } catch (err) { 42 | console.error("Stored auth parsing error:", err); 43 | } 44 | } 45 | 46 | try { 47 | const nonce = new String(Date.now()); 48 | const nonceBuffer = Buffer.from( 49 | new TextEncoder().encode(nonce.padStart(32, "0")), 50 | ); 51 | 52 | const message = "Login to NEAR AI"; 53 | const recipient = "ai.near"; // use actual recipient used by your backend 54 | const callbackUrl = location.href; 55 | 56 | const result = await signMessage({ 57 | message, 58 | recipient, 59 | nonce: nonceBuffer 60 | }); 61 | 62 | const auth = { 63 | message, 64 | nonce: nonce, 65 | recipient: recipient, 66 | callback_url: callbackUrl, 67 | signature: result.signature, 68 | account_id: result.accountId, 69 | public_key: result.publicKey, 70 | }; 71 | 72 | setNearSignatureAuth(auth); 73 | localStorage.setItem("NearAIAuthObject", JSON.stringify(auth)); 74 | } catch (err) { 75 | console.error("Error signing in:", err); 76 | setError("Authentication failed."); 77 | } 78 | }; 79 | 80 | authenticate(); 81 | }, [signedAccountId, signMessage]); 82 | 83 | const handleStartChat = async (e) => { 84 | e.preventDefault(); 85 | if (!agentId.trim()) { 86 | setError("Please enter an Agent ID"); 87 | return; 88 | } 89 | 90 | try { 91 | setLoading(true); 92 | setError(null); 93 | 94 | const newThread = await createThread(nearSignatureAuth); 95 | if (newThread?.id) { 96 | setThreadId(newThread.id); 97 | setMessages([]); 98 | } else { 99 | setError("Failed to create thread"); 100 | } 101 | } catch (err) { 102 | console.error(err); 103 | setError("Error starting chat"); 104 | } finally { 105 | setLoading(false); 106 | } 107 | }; 108 | 109 | const handleSendMessage = async (e) => { 110 | e.preventDefault(); 111 | if (!message.trim() || !threadId || !agentId) return; 112 | 113 | const userMsg = { 114 | id: Date.now().toString(), 115 | role: "user", 116 | content: [{ type: "text", text: { value: message } }], 117 | created_at: Date.now(), 118 | }; 119 | 120 | try { 121 | setLoading(true); 122 | setMessages((prev) => [...prev, userMsg]); 123 | 124 | await runAgent(nearSignatureAuth, agentId, threadId, message); 125 | const updatedThread = await fetchThreadState(nearSignatureAuth, threadId); 126 | 127 | if (updatedThread?.data) { 128 | setMessages((prevMessages) => [ 129 | ...prevMessages, 130 | ...updatedThread.data 131 | .filter( 132 | (msg) => 133 | (msg.role === "assistant" && msg.metadata === null) || msg.role === "user" 134 | ) 135 | .map((msg) => ({ 136 | id: msg.id, 137 | role: msg.role, 138 | content: msg.content, 139 | created_at: msg.created_at, 140 | })), 141 | ]); 142 | 143 | } 144 | 145 | setMessage(""); 146 | } catch (err) { 147 | console.error(err); 148 | setError("Failed to send message"); 149 | } finally { 150 | setLoading(false); 151 | } 152 | }; 153 | 154 | const formatMessageContent = (content) => 155 | Array.isArray(content) 156 | ? content.map((item, index) => 157 | item.type === "text" ?

{item.text?.value}

: null 158 | ) 159 | : ""; 160 | 161 | return ( 162 |
163 |
164 |

Agent Chat

165 |

Interact with AI agents on NEAR

166 |
167 | 168 |
169 | {!threadId ? ( 170 |
171 |
172 | 175 | setAgentId(e.target.value)} 180 | placeholder="e.g., ai-creator.near/BlackJack_Dealer/0.0.2" 181 | className={styles.input} 182 | required 183 | /> 184 |
185 | 192 |
193 | ) : ( 194 | <> 195 |
196 |

Chatting with: {agentId}

197 | 203 |
204 | 205 |
206 | {messages.length === 0 ? ( 207 |

208 | Send a message to start the conversation 209 |

210 | ) : ( 211 | [...messages].map((msg) => ( 212 |
221 |
222 | {msg.role === "user" 223 | ? "You" 224 | : msg.role === "assistant" 225 | ? "Agent" 226 | : "System"} 227 |
228 |
229 | {formatMessageContent(msg.content)} 230 |
231 |
232 | )) 233 | )} 234 |
235 |
236 | 237 |
238 |