Skip to content

Commit 8c1b89d

Browse files
yinkscsscursoragent
andcommitted
feat: smart airdrop with faucet link, markdown chat, onboarding flow
- Replace unreliable devnet RPC airdrop with direct faucet.solana.com link containing user's wallet address pre-filled - Add react-markdown rendering for assistant messages (bold, links, code, lists) - Add first-time onboarding: welcome message with wallet address and faucet link - Stop Prometheus metrics spam by stopping the container (services lack /metrics) - Update system prompt with airdrop instructions for faucet link presentation Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1c4fc00 commit 8c1b89d

6 files changed

Lines changed: 258 additions & 86 deletions

File tree

apps/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"radix-ui": "^1.4.3",
1919
"react": "19.2.3",
2020
"react-dom": "19.2.3",
21+
"react-markdown": "^10.1.0",
2122
"tailwind-merge": "^3.5.0"
2223
},
2324
"devDependencies": {

apps/dashboard/src/app/api/agent-execute/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ NEVER execute a transfer or swap without asking first.
3030
- If a transfer fails, explain why in simple terms and suggest a fix
3131
- If the user asks something you can't do, say so honestly
3232
33+
### Airdrops
34+
When a user asks for test SOL or an airdrop, use the request_airdrop tool. It will give you a faucet link. Present it to the user as a clickable markdown link and show their wallet address. Example:
35+
"Here's your wallet address: \`abc123...\`
36+
37+
Click here to get free test SOL: [Get SOL from Faucet](https://faucet.solana.com/?address=abc123&network=devnet)
38+
39+
The faucet will send free devnet SOL to your wallet. Just click the link and confirm!"
40+
3341
### Formatting
3442
- Use backticks for addresses and transaction IDs
3543
- Use **bold** for amounts

apps/dashboard/src/app/dashboard/page.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import {
2222
ReadOnlyToolBadge,
2323
type ToolCallInfo,
2424
} from '@/components/chat/confirmation-card';
25+
import { ChatMarkdown } from '@/components/chat/chat-markdown';
2526
import type { Agent } from '@/types';
2627

27-
const ACTIONABLE_TOOLS = new Set(['transfer', 'swap', 'request_airdrop', 'create_wallet']);
28+
const ACTIONABLE_TOOLS = new Set(['transfer', 'swap', 'create_wallet']);
2829

2930
interface ChatMessage {
3031
id: string;
@@ -124,6 +125,29 @@ function DashboardChat() {
124125
return () => window.removeEventListener('solagent-new-chat', handler);
125126
}, []);
126127

128+
const onboardingDone = useRef(false);
129+
useEffect(() => {
130+
if (onboardingDone.current || chatId || messages.length > 0) return;
131+
if (!walletPublicKey || !selectedAgentId) return;
132+
133+
const hasHistory = localStorage.getItem('solagent-onboarded');
134+
if (hasHistory) return;
135+
onboardingDone.current = true;
136+
137+
const addr = walletPublicKey;
138+
const truncated = `${addr.slice(0, 4)}...${addr.slice(-4)}`;
139+
const faucetUrl = `https://faucet.solana.com/?address=${addr}&network=devnet`;
140+
141+
setMessages([
142+
{
143+
id: crypto.randomUUID(),
144+
role: 'assistant',
145+
content: `Hey! Welcome to **SolAgent** — your AI assistant for Solana.\n\nI've set up a devnet wallet for you: \`${truncated}\`\n\nTo get started, grab some free test SOL:\n\n[Get free SOL from faucet](${faucetUrl})\n\nOnce you have SOL, you can ask me to:\n- **Check your balance**\n- **Send SOL** to any address\n- **Swap tokens** via Jupiter\n\nWhat would you like to do?`,
146+
},
147+
]);
148+
localStorage.setItem('solagent-onboarded', 'true');
149+
}, [walletPublicKey, selectedAgentId, chatId, messages.length]);
150+
127151
useEffect(() => {
128152
requestAnimationFrame(() => {
129153
scrollRef.current?.scrollTo({
@@ -367,7 +391,9 @@ function DashboardChat() {
367391
</div>
368392
) : (
369393
<div className="text-sm leading-relaxed text-foreground">
370-
<p className="whitespace-pre-wrap break-words">{msg.content}</p>
394+
<div className="prose-sm">
395+
<ChatMarkdown content={msg.content} />
396+
</div>
371397
{msg.toolCalls && msg.toolCalls.length > 0 && (
372398
<div className="mt-2 space-y-2">
373399
{msg.toolCalls.map((tc, i) =>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use client';
2+
3+
import ReactMarkdown from 'react-markdown';
4+
5+
interface ChatMarkdownProps {
6+
content: string;
7+
}
8+
9+
export function ChatMarkdown({ content }: ChatMarkdownProps) {
10+
return (
11+
<ReactMarkdown
12+
components={{
13+
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
14+
a: ({ href, children }) => (
15+
<a
16+
href={href}
17+
target="_blank"
18+
rel="noopener noreferrer"
19+
className="text-violet-400 underline underline-offset-2 hover:text-violet-300 transition-colors"
20+
>
21+
{children}
22+
</a>
23+
),
24+
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
25+
code: ({ children, className }) => {
26+
const isBlock = className?.includes('language-');
27+
if (isBlock) {
28+
return (
29+
<code className="block my-2 rounded-lg bg-secondary/50 px-3 py-2 text-xs font-mono overflow-x-auto">
30+
{children}
31+
</code>
32+
);
33+
}
34+
return (
35+
<code className="rounded bg-secondary/50 px-1.5 py-0.5 text-xs font-mono">
36+
{children}
37+
</code>
38+
);
39+
},
40+
ul: ({ children }) => (
41+
<ul className="mb-2 ml-4 list-disc space-y-1 last:mb-0">{children}</ul>
42+
),
43+
ol: ({ children }) => (
44+
<ol className="mb-2 ml-4 list-decimal space-y-1 last:mb-0">{children}</ol>
45+
),
46+
li: ({ children }) => <li>{children}</li>,
47+
}}
48+
>
49+
{content}
50+
</ReactMarkdown>
51+
);
52+
}

0 commit comments

Comments
 (0)