AI.tsx raw
1 import {
2 ArrowRightIcon,
3 ArrowUpRightIcon,
4 BotIcon,
5 BoxIcon,
6 CheckCircleIcon,
7 ChevronRightIcon,
8 CopyIcon,
9 EyeOffIcon,
10 HammerIcon,
11 InfoIcon,
12 LayersIcon,
13 LayoutGridIcon,
14 type LucideIcon,
15 RepeatIcon,
16 ShieldCheckIcon,
17 ShoppingBagIcon,
18 SparklesIcon,
19 XIcon,
20 ZapIcon,
21 } from "lucide-react";
22 import React from "react";
23 import { Link, useNavigate } from "react-router";
24 import { toast } from "sonner";
25 import bitrefillLogo from "src/assets/suggested-apps/bitrefill.png";
26 import claudeLogo from "src/assets/suggested-apps/claude.png";
27 import clineLogo from "src/assets/suggested-apps/cline.png";
28 import codexLogo from "src/assets/suggested-apps/codex.png";
29 import cursorLogo from "src/assets/suggested-apps/cursor.png";
30 import geminiLogo from "src/assets/suggested-apps/gemini.png";
31 import gooseLogo from "src/assets/suggested-apps/goose.png";
32 import hermesLogo from "src/assets/suggested-apps/hermes.png";
33 import openclawLogo from "src/assets/suggested-apps/openclaw.png";
34 import opencodeLogo from "src/assets/suggested-apps/opencode.png";
35 import payperqLogo from "src/assets/suggested-apps/payperq.png";
36 import piLogo from "src/assets/suggested-apps/pi.png";
37 import AppHeader from "src/components/AppHeader";
38 import ExternalLink from "src/components/ExternalLink";
39 import Loading from "src/components/Loading";
40 import { Button } from "src/components/ui/button";
41 import {
42 Card,
43 CardContent,
44 CardHeader,
45 CardTitle,
46 } from "src/components/ui/card";
47 import { LinkButton } from "src/components/ui/custom/link-button";
48 import {
49 Select,
50 SelectContent,
51 SelectItem,
52 SelectTrigger,
53 SelectValue,
54 } from "src/components/ui/select";
55 import {
56 Tabs,
57 TabsContent,
58 TabsList,
59 TabsTrigger,
60 } from "src/components/ui/tabs";
61 import {
62 DEFAULT_APP_BUDGET_RENEWAL,
63 DEFAULT_APP_BUDGET_SATS,
64 localStorageKeys,
65 } from "src/constants";
66 import { useInfo } from "src/hooks/useInfo";
67 import { copyToClipboard } from "src/lib/clipboard";
68 import { createApp } from "src/requests/createApp";
69 import { handleRequestError } from "src/utils/handleRequestError";
70
71 type Agent = {
72 id: string;
73 name: string;
74 logo: string;
75 description: string;
76 setupUrl: string;
77 mcpInstructions?: () => React.ReactNode;
78 };
79
80 const agents: Agent[] = [
81 {
82 id: "openclaw",
83 name: "OpenClaw",
84 logo: openclawLogo,
85 description: "Open-source personal AI assistant",
86 setupUrl: "",
87 },
88 {
89 id: "hermes",
90 name: "Hermes",
91 logo: hermesLogo,
92 description: "Self-improving open-source AI agent by Nous Research",
93 setupUrl: "",
94 },
95 {
96 id: "cursor",
97 name: "Cursor",
98 logo: cursorLogo,
99 description: "AI-powered code editor",
100 setupUrl: "",
101 },
102 {
103 id: "claude",
104 name: "Claude",
105 logo: claudeLogo,
106 description: "Anthropic's AI assistant (Code, Web & Desktop)",
107 setupUrl: "",
108 mcpInstructions: () => (
109 <ol className="list-decimal list-inside space-y-1 text-sm">
110 <li>
111 Open{" "}
112 <a
113 href="https://claude.ai"
114 target="_blank"
115 className="underline font-medium"
116 >
117 claude.ai
118 </a>{" "}
119 or Claude Desktop and sign in
120 </li>
121 <li>Go to Settings → Connectors</li>
122 <li>Add custom connector</li>
123 <li>Enter “Alby” as connector name</li>
124 <li>Paste the URL below as the connector URL</li>
125 </ol>
126 ),
127 },
128 {
129 id: "gemini",
130 name: "Gemini CLI",
131 logo: geminiLogo,
132 description: "Google's open-source AI agent for the terminal",
133 setupUrl: "",
134 },
135 {
136 id: "codex",
137 name: "Codex",
138 logo: codexLogo,
139 description: "OpenAI's coding agent",
140 setupUrl: "",
141 },
142 {
143 id: "cline",
144 name: "Cline",
145 logo: clineLogo,
146 description: "AI coding assistant for VS Code",
147 setupUrl: "",
148 },
149 {
150 id: "goose",
151 name: "Goose",
152 logo: gooseLogo,
153 description: "Local AI agent by Block for automating engineering tasks",
154 setupUrl: "",
155 },
156 {
157 id: "opencode",
158 name: "OpenCode",
159 logo: opencodeLogo,
160 description: "Terminal-based AI coding assistant",
161 setupUrl: "",
162 },
163 {
164 id: "pi",
165 name: "Pi",
166 logo: piLogo,
167 description: "Minimal terminal coding agent, great for local LLMs",
168 setupUrl: "",
169 },
170 ];
171
172 export function AI() {
173 const navigate = useNavigate();
174 const [selectedAgent, setSelectedAgent] = React.useState<string | null>(null);
175 const [heroDismissed, setHeroDismissed] = React.useState(
176 () => localStorage.getItem(localStorageKeys.aiHeroDismissed) === "true"
177 );
178
179 const dismissHero = React.useCallback(() => {
180 setHeroDismissed(true);
181 localStorage.setItem(localStorageKeys.aiHeroDismissed, "true");
182 }, []);
183
184 const selectedAgentData = agents.find((a) => a.id === selectedAgent);
185 const showAuthPrompt = selectedAgent && !selectedAgentData?.setupUrl;
186
187 const handleConnect = () => {
188 if (selectedAgentData?.setupUrl) {
189 navigate(selectedAgentData.setupUrl);
190 }
191 };
192
193 return (
194 <>
195 <AppHeader
196 title="AI & Agents"
197 pageTitle="AI & Agents"
198 contentRight={
199 <LinkButton to="/apps?tab=app-store&category=ai" variant="outline">
200 <LayoutGridIcon className="w-4 h-4" />
201 Explore App Store
202 </LinkButton>
203 }
204 />
205
206 {/* Hero — collapsible, persisted in localStorage */}
207 {!heroDismissed && (
208 <div className="overflow-hidden">
209 <div className="bg-card text-card-foreground rounded-xl overflow-hidden relative border border-border">
210 <button
211 onClick={dismissHero}
212 className="absolute top-4 right-4 z-10 text-muted-foreground hover:text-foreground transition-colors"
213 aria-label="Dismiss"
214 >
215 <XIcon className="w-5 h-5" />
216 </button>
217 <div className="flex flex-col lg:flex-row min-h-[360px]">
218 {/* Left */}
219 <div className="flex-1 p-8 lg:p-12 flex flex-col justify-center">
220 <p className="text-muted-foreground text-sm font-medium tracking-widest uppercase mb-4">
221 AI + Bitcoin
222 </p>
223 <h2 className="text-4xl lg:text-5xl font-bold tracking-tight leading-[1.1] mb-4">
224 Give your AI agent
225 <br />
226 <span className="text-primary">a wallet</span>
227 </h2>
228 <p className="text-muted-foreground text-lg max-w-md">
229 Connect any AI agent to your Alby Hub and let it send
230 payments, buy gift cards, and access paid services on its own.
231 </p>
232 </div>
233
234 {/* Right — terminal mockup */}
235 <div className="flex-1 p-6 pt-12 lg:p-8 lg:pt-14 flex items-center">
236 <div className="w-full max-w-2xl ml-auto bg-muted rounded-lg border border-border overflow-hidden shadow-2xl">
237 <div className="flex items-center gap-1.5 px-4 py-2.5 border-b border-border">
238 <div className="w-2.5 h-2.5 rounded-full bg-muted-foreground/30" />
239 <div className="w-2.5 h-2.5 rounded-full bg-muted-foreground/30" />
240 <div className="w-2.5 h-2.5 rounded-full bg-muted-foreground/30" />
241 <span className="text-muted-foreground text-xs ml-2 font-mono">
242 claude
243 </span>
244 </div>
245 <div className="p-4 font-mono text-sm space-y-3">
246 <div>
247 <span className="text-muted-foreground">> </span>
248 <span className="text-foreground">
249 Buy a $15 DoorDash gift card
250 </span>
251 </div>
252 <div className="text-muted-foreground pl-3 border-l-2 border-primary/40 space-y-1">
253 <p>
254 <SparklesIcon className="w-3 h-3 inline text-primary mr-1" />
255 Searching Bitrefill for DoorDash...
256 </p>
257 <p>
258 <ZapIcon className="w-3 h-3 inline text-primary mr-1" />
259 Paying 45,210 sats via Lightning
260 </p>
261 <p>
262 <CheckCircleIcon className="w-3 h-3 inline text-positive-foreground mr-1" />
263 <span className="text-positive-foreground">Done!</span>{" "}
264 Gift card code: XXXX-XXXX-XXXX
265 </p>
266 </div>
267 <div>
268 <span className="text-muted-foreground">> </span>
269 <span className="text-foreground">
270 Send $5 to hub@getalby.com
271 </span>
272 </div>
273 <div className="text-muted-foreground pl-3 border-l-2 border-primary/40 space-y-1">
274 <p>
275 <ZapIcon className="w-3 h-3 inline text-primary mr-1" />
276 Sending 15,000 sats...
277 </p>
278 <p>
279 <CheckCircleIcon className="w-3 h-3 inline text-positive-foreground mr-1" />
280 <span className="text-positive-foreground">Sent!</span>{" "}
281 Payment confirmed
282 </p>
283 </div>
284 <div className="flex items-center">
285 <span className="text-muted-foreground">> </span>
286 <span className="w-2 h-4 bg-primary ml-0.5 animate-[blink_1s_step-end_3]" />
287 </div>
288 </div>
289 </div>
290 </div>
291 </div>
292
293 {/* Why Lightning — value props */}
294 <div className="border-t border-border grid grid-cols-1 sm:grid-cols-3 divide-y sm:divide-y-0 sm:divide-x divide-border">
295 {whyLightningItems.map((item) => {
296 const Icon = item.icon;
297 return (
298 <div key={item.title} className="p-6 lg:p-8">
299 <Icon className="w-5 h-5 text-primary mb-2" />
300 <h3 className="font-semibold text-lg mb-1">{item.title}</h3>
301 <p className="text-muted-foreground text-sm">
302 {item.description}
303 </p>
304 </div>
305 );
306 })}
307 </div>
308 </div>
309 </div>
310 )}
311
312 {/* Connect section */}
313 <div className="space-y-4">
314 <Card className="border-primary/75 bg-primary/5">
315 <CardHeader>
316 <div className="flex items-start justify-between">
317 <div className="flex items-center gap-3">
318 {selectedAgentData ? (
319 <img
320 src={selectedAgentData.logo}
321 alt={selectedAgentData.name}
322 className="w-10 h-10 rounded-lg shrink-0"
323 />
324 ) : (
325 <div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center shrink-0">
326 <BotIcon className="w-5 h-5 text-primary" />
327 </div>
328 )}
329 <div>
330 <CardTitle>Connect Your Agent</CardTitle>
331 <p className="text-sm text-muted-foreground mt-1">
332 Pick your agent, create a connection, and follow the setup
333 steps
334 </p>
335 </div>
336 </div>
337 <Link
338 to="/apps?tab=connected-apps"
339 className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
340 >
341 Manage Connections
342 <ArrowRightIcon className="w-3 h-3" />
343 </Link>
344 </div>
345 </CardHeader>
346 <CardContent className="space-y-4">
347 <div className="flex items-center gap-2">
348 <Select
349 value={selectedAgent ?? undefined}
350 onValueChange={(value) => {
351 setSelectedAgent(value);
352 }}
353 >
354 <SelectTrigger className="w-60">
355 <SelectValue
356 placeholder={
357 <span className="flex items-center gap-2">
358 <span className="flex -space-x-2">
359 {agents.slice(0, 4).map((agent) => (
360 <img
361 key={agent.id}
362 src={agent.logo}
363 alt={agent.name}
364 className="w-5 h-5 rounded border-2 border-background"
365 />
366 ))}
367 </span>
368 <span className="text-muted-foreground">
369 Choose your agent
370 </span>
371 </span>
372 }
373 />
374 </SelectTrigger>
375 <SelectContent>
376 {agents.map((agent) => (
377 <SelectItem key={agent.id} value={agent.id}>
378 <div className="flex items-center gap-2">
379 <img
380 src={agent.logo}
381 alt={agent.name}
382 className="w-5 h-5 rounded"
383 />
384 {agent.name}
385 </div>
386 </SelectItem>
387 ))}
388 <SelectItem value="other">Other</SelectItem>
389 </SelectContent>
390 </Select>
391 {selectedAgentData?.setupUrl && (
392 <Button onClick={handleConnect}>
393 <ZapIcon className="w-4 h-4" />
394 Connect
395 </Button>
396 )}
397 </div>
398
399 {/* Auth prompt shown immediately for CLI agents */}
400 {showAuthPrompt && (
401 <GenericAuthPrompt
402 agent={
403 selectedAgentData ?? {
404 id: "other",
405 name: "your agent",
406 logo: "",
407 description: "",
408 setupUrl: "",
409 }
410 }
411 />
412 )}
413 </CardContent>
414 </Card>
415
416 {/* Inspiration */}
417 <InspirationPrompts />
418
419 {/* Featured services — branded full cards */}
420 <div>
421 <h2 className="text-2xl font-bold mb-4">Featured Services</h2>
422 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
423 <ExternalLink to="https://www.bitrefill.com/agents">
424 <Card className="group relative h-full hover:border-primary/30 transition-colors p-0">
425 <CardContent className="p-4 flex flex-col h-full">
426 <div className="flex items-center gap-3 mb-3">
427 <img
428 src={bitrefillLogo}
429 alt="Bitrefill"
430 className="w-10 h-10 rounded-lg"
431 />
432 <div className="flex-1">
433 <p className="font-semibold">Bitrefill</p>
434 <p className="text-xs text-muted-foreground">
435 Gift cards & e-SIMs
436 </p>
437 </div>
438 </div>
439 <ArrowUpRightIcon className="w-4 h-4 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors absolute top-4 right-4" />
440 <p className="text-sm text-muted-foreground flex-1">
441 "Buy a $15 DoorDash gift card" — your agent pays
442 via Lightning and delivers the code.
443 </p>
444 <p className="text-xs text-muted-foreground/60 mt-3 font-mono">
445 MCP Server
446 </p>
447 </CardContent>
448 </Card>
449 </ExternalLink>
450
451 <ExternalLink to="https://ppq.ai/invite/3f21c1e5">
452 <Card className="group relative h-full hover:border-primary/30 transition-colors p-0">
453 <CardContent className="p-4 flex flex-col h-full">
454 <div className="flex items-center gap-3 mb-3">
455 <img
456 src={payperqLogo}
457 alt="PPQ.ai"
458 className="w-10 h-10 rounded-lg"
459 />
460 <div className="flex-1">
461 <p className="font-semibold">PPQ.ai</p>
462 <p className="text-xs text-muted-foreground">
463 AI model access
464 </p>
465 </div>
466 </div>
467 <ArrowUpRightIcon className="w-4 h-4 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors absolute top-4 right-4" />
468 <p className="text-sm text-muted-foreground flex-1">
469 Pay-per-prompt access to top AI models. No subscription —
470 pay with Lightning.
471 </p>
472 <p className="text-xs text-muted-foreground/60 mt-3 font-mono">
473 NWC Auto Top-up
474 </p>
475 </CardContent>
476 </Card>
477 </ExternalLink>
478
479 <ExternalLink to="https://402index.io">
480 <Card className="group relative h-full hover:border-primary/30 transition-colors p-0">
481 <CardContent className="p-4 flex flex-col h-full">
482 <div className="flex items-center gap-3 mb-3">
483 <div className="w-10 h-10 rounded-lg bg-[#7c8aff]/10 flex items-center justify-center">
484 <LayersIcon className="w-5 h-5 text-[#7c8aff]" />
485 </div>
486 <div className="flex-1">
487 <p className="font-semibold">402 Index</p>
488 <p className="text-xs text-muted-foreground">
489 1,000+ APIs
490 </p>
491 </div>
492 </div>
493 <ArrowUpRightIcon className="w-4 h-4 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors absolute top-4 right-4" />
494 <p className="text-sm text-muted-foreground flex-1">
495 Directory of paid API endpoints — search, data, compute,
496 LLMs, and more.
497 </p>
498 <p className="text-xs text-muted-foreground/60 mt-3 font-mono">
499 L402 / x402 / MPP
500 </p>
501 </CardContent>
502 </Card>
503 </ExternalLink>
504 </div>
505 </div>
506 </div>
507 </>
508 );
509 }
510
511 function GenericAuthPrompt({ agent }: { agent: Agent }) {
512 const [mcpMode, setMcpMode] = React.useState(false);
513 const basePath = import.meta.env.BASE_URL.replace(/\/$/, "");
514 const hubUrl = window.location.origin + basePath;
515 const genericPrompt = `Run \`npx -y skills add getAlby/payments-skill -y\` and connect to my Alby Hub wallet at ${hubUrl}`;
516
517 return (
518 <div
519 key={agent.id}
520 className="space-y-3 text-sm animate-[flash_0.4s_ease-out]"
521 >
522 {mcpMode && agent.mcpInstructions ? (
523 <McpSetup
524 agentName={agent.name}
525 mcpInstructions={agent.mcpInstructions}
526 onBack={() => setMcpMode(false)}
527 />
528 ) : (
529 <>
530 <p className="text-muted-foreground">
531 Copy this prompt and paste it into {agent.name}:
532 </p>
533 <button
534 onClick={() => copyToClipboard(genericPrompt)}
535 className="flex items-center gap-3 rounded-lg bg-muted/50 border border-border px-4 py-3 w-full text-left cursor-pointer hover:border-primary/30 transition-colors"
536 >
537 <ChevronRightIcon className="w-3.5 h-3.5 text-muted-foreground shrink-0 self-start mt-0.5" />
538 <p className="flex-1 text-sm font-mono break-all select-none">
539 {genericPrompt}
540 </p>
541 <CopyIcon className="w-4 h-4 text-muted-foreground shrink-0" />
542 </button>
543 {agent.mcpInstructions && (
544 <p className="text-xs text-muted-foreground">
545 <button
546 onClick={() => setMcpMode(true)}
547 className="underline hover:text-foreground transition-colors"
548 >
549 Using {agent.name} Web / Desktop?
550 </button>
551 </p>
552 )}
553 </>
554 )}
555 </div>
556 );
557 }
558
559 function McpSetup({
560 agentName,
561 mcpInstructions,
562 onBack,
563 }: {
564 agentName: string;
565 mcpInstructions: () => React.ReactNode;
566 onBack: () => void;
567 }) {
568 const [isLoading, setIsLoading] = React.useState(false);
569 const [mcpUrl, setMcpUrl] = React.useState("");
570 const [createdAppId, setCreatedAppId] = React.useState<number>();
571
572 const handleCreateConnection = React.useCallback(async () => {
573 if (isLoading || mcpUrl) {
574 return;
575 }
576 setIsLoading(true);
577 try {
578 const response = await createApp({
579 name: agentName,
580 scopes: [
581 "get_info",
582 "get_balance",
583 "list_transactions",
584 "lookup_invoice",
585 "make_invoice",
586 "notifications",
587 "pay_invoice",
588 "sign_message",
589 ],
590 maxAmountSat: DEFAULT_APP_BUDGET_SATS,
591 budgetRenewal: DEFAULT_APP_BUDGET_RENEWAL,
592 metadata: {
593 app_store_app_id: agentName.toLowerCase().replace(/\s+/g, "-"),
594 },
595 });
596 setMcpUrl(
597 `https://mcp.getalby.com/mcp?nwc=${encodeURIComponent(response.pairingUri)}`
598 );
599 setCreatedAppId(response.id);
600 toast("Connection created");
601 } catch (error) {
602 handleRequestError("Failed to create connection", error);
603 }
604 setIsLoading(false);
605 }, [agentName, isLoading, mcpUrl]);
606
607 // Create connection immediately when the component mounts
608 React.useEffect(() => {
609 handleCreateConnection();
610 }, [handleCreateConnection]);
611
612 if (isLoading) {
613 return (
614 <div className="flex items-center gap-2 text-sm text-muted-foreground">
615 <Loading className="size-4" />
616 Creating connection...
617 </div>
618 );
619 }
620
621 if (!mcpUrl) {
622 return (
623 <Button onClick={handleCreateConnection} size="sm">
624 Retry
625 </Button>
626 );
627 }
628
629 const maskedUrl = `https://mcp.getalby.com/mcp?nwc=${"•".repeat(20)}`;
630
631 return (
632 <div className="space-y-3 text-sm">
633 {mcpInstructions()}
634 <button
635 onClick={() => copyToClipboard(mcpUrl)}
636 className="flex items-center gap-2 rounded-lg bg-muted/50 border border-border px-3 py-2 w-full text-left cursor-pointer hover:border-primary/30 transition-colors"
637 >
638 <p className="flex-1 text-xs font-mono select-none truncate">
639 {maskedUrl}
640 </p>
641 <CopyIcon className="w-4 h-4 text-muted-foreground shrink-0" />
642 </button>
643 <div className="flex items-center gap-3">
644 {createdAppId && (
645 <Link
646 to={`/apps/${createdAppId}`}
647 className="text-xs text-muted-foreground hover:text-foreground underline transition-colors"
648 >
649 Edit budget & permissions
650 </Link>
651 )}
652 <button
653 onClick={onBack}
654 className="text-xs text-muted-foreground hover:text-foreground underline transition-colors"
655 >
656 Use CLI instead
657 </button>
658 </div>
659 </div>
660 );
661 }
662
663 const whyLightningItems = [
664 {
665 icon: ShieldCheckIcon,
666 title: "Stay in Control",
667 description:
668 "Set spending limits per agent. You decide how much it can spend and when budgets reset — no surprise bills.",
669 },
670 {
671 icon: EyeOffIcon,
672 title: "Private by Default",
673 description:
674 "No credit cards or personal info shared with merchants. Your agent pays over lightning — fast, direct, and private.",
675 },
676 {
677 icon: ZapIcon,
678 title: "Instant Access to Paid Services",
679 description:
680 "Your agent can access 1,000+ paid APIs instantly — gift cards, domains, hosting, AI models, and more.",
681 },
682 ];
683
684 function getInspirationCategories(hasChannelManagement: boolean): {
685 label: string;
686 icon: LucideIcon;
687 prompts: string[];
688 skill?: { prompt: string; skillName: string; url: string };
689 }[] {
690 return [
691 {
692 label: "Wallet",
693 icon: ZapIcon,
694 prompts: [
695 "send $5 to hub@getalby.com for coffee",
696 "how much is $10 in sats right now?",
697 "make an invoice for 50,000 sats",
698 ],
699 },
700 {
701 label: "Shopping",
702 icon: ShoppingBagIcon,
703 prompts: [
704 "buy a $25 Netflix gift card on bitrefill.com",
705 "get me an eSIM with 5GB of data for my trip to Portugal on bitrefill.com",
706 "what gift cards are available in the US on bitrefill.com?",
707 ],
708 skill: {
709 prompt:
710 "Run `npx -y skills add bitrefill/agents -y` to install the Bitrefill Skill",
711 skillName: "Bitrefill Skill",
712 url: "https://bitrefill.com/agents",
713 },
714 },
715 {
716 label: "Creative",
717 icon: SparklesIcon,
718 prompts: [
719 "generate a watercolor painting of a mountain cabin at sunset on ppq.ai",
720 "generate a cool bitcoin logo on ppq.ai and print it on a t-shirt on unhuman.store",
721 "create a logo for my coffee shop using ppq.ai image generation",
722 ],
723 },
724 {
725 label: "Services",
726 icon: LayersIcon,
727 prompts: [
728 "search podcasts for discussions about bitcoin scaling",
729 "set up an anonymous email address on lnemail.net",
730 "buy the domain my-awesome-project.dev on unhuman.domains",
731 "spin up a VPS with 2 cores and 4GB RAM on lnvps.net",
732 ],
733 },
734 {
735 label: "Automation",
736 icon: RepeatIcon,
737 prompts: [
738 "read payouts.csv and send 1,000 sats to each lightning address",
739 "calculate how much I spent this month and break it down by day",
740 "export all my transactions from the last 12 months as a CSV",
741 ],
742 },
743 {
744 label: "Build Apps",
745 icon: HammerIcon,
746 prompts: [
747 "build an AI image generator that charges 500 sats per image",
748 "create a file converter that charges 50 sats per conversion",
749 "build a blog where readers unlock articles for 50 sats each",
750 ],
751 skill: {
752 prompt:
753 "Run `npx -y skills add getAlby/builder-skill -y` to install the Builder Skill",
754 skillName: "Builder Skill",
755 url: "https://github.com/getAlby/builder-skill",
756 },
757 },
758 {
759 label: "Alby Hub",
760 icon: BoxIcon,
761 prompts: [
762 "create a sub-wallet for my mum",
763 "setup a new alby hub on my VPS",
764 "give me an on-chain deposit address",
765 "create a new app connection with a 21,000 sat monthly budget",
766 "list all my app connections and their budgets",
767 "make a read-only connection I can share with my accountant",
768 'revoke the connection called "old laptop"',
769 ...(hasChannelManagement
770 ? [
771 "open a channel with 2M sats to ACINQ's node",
772 "show me my channels and their balances",
773 "what's my node's connection info?",
774 ]
775 : []),
776 ],
777 skill: {
778 prompt:
779 "Run `npx -y skills add getAlby/hub-skill -y` to install the Alby Hub Skill",
780 skillName: "Alby Hub Skill",
781 url: "https://github.com/getAlby/hub-skill",
782 },
783 },
784 ];
785 }
786
787 function RotatingPrompt({ prompts }: { prompts: string[] }) {
788 const [index, setIndex] = React.useState(0);
789 const [charCount, setCharCount] = React.useState(0);
790 const [isTyping, setIsTyping] = React.useState(true);
791
792 const currentPrompt = prompts[index];
793
794 // Reset when prompts change (tab switch)
795 React.useEffect(() => {
796 setIndex(0);
797 setCharCount(0);
798 setIsTyping(true);
799 }, [prompts]);
800
801 // Typing effect — advance characters
802 React.useEffect(() => {
803 if (!isTyping) {
804 return;
805 }
806 if (charCount >= currentPrompt.length) {
807 setIsTyping(false);
808 return;
809 }
810 const timeout = setTimeout(() => {
811 setCharCount((c) => c + 1);
812 }, 30);
813 return () => clearTimeout(timeout);
814 }, [charCount, currentPrompt, isTyping]);
815
816 // Pause then rotate to next prompt
817 React.useEffect(() => {
818 if (isTyping) {
819 return;
820 }
821 const pause = setTimeout(() => {
822 setIndex((i) => (i + 1) % prompts.length);
823 setCharCount(0);
824 setIsTyping(true);
825 }, 3000);
826 return () => clearTimeout(pause);
827 }, [isTyping, prompts]);
828
829 return (
830 <button
831 onClick={() => copyToClipboard(currentPrompt)}
832 className="flex items-center gap-3 rounded-lg bg-muted/50 border border-border px-4 py-3 w-full text-left cursor-pointer hover:border-primary/30 transition-colors"
833 >
834 <ChevronRightIcon className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
835 <p className="flex-1 text-sm font-mono">
836 {currentPrompt.slice(0, charCount)}
837 <span
838 className={`inline-block w-2 h-[1.1em] translate-y-[2px] ml-0.5 bg-primary ${
839 isTyping ? "" : "animate-[blink_1s_step-end_infinite]"
840 }`}
841 />
842 </p>
843 <CopyIcon className="w-4 h-4 text-muted-foreground shrink-0" />
844 </button>
845 );
846 }
847
848 function InspirationPrompts() {
849 const { hasChannelManagement } = useInfo();
850 const inspirationCategories =
851 getInspirationCategories(!!hasChannelManagement);
852
853 return (
854 <Tabs defaultValue={inspirationCategories[0].label}>
855 <div className="rounded-xl border border-border bg-card overflow-hidden">
856 <div className="px-5 pt-5 pb-4">
857 <p className="font-semibold text-sm mb-4">What can your agent do?</p>
858 <TabsList variant="line">
859 {inspirationCategories.map((cat) => {
860 const Icon = cat.icon;
861 return (
862 <TabsTrigger
863 key={cat.label}
864 value={cat.label}
865 className="gap-1.5"
866 >
867 <Icon className="w-3.5 h-3.5 translate-y-px" />
868 {cat.label}
869 </TabsTrigger>
870 );
871 })}
872 </TabsList>
873 </div>
874
875 <div className="px-5 pb-5">
876 {inspirationCategories.map((cat) => (
877 <TabsContent
878 key={cat.label}
879 value={cat.label}
880 className="mt-0 space-y-2"
881 >
882 <RotatingPrompt prompts={cat.prompts} />
883 {cat.skill && (
884 <div className="flex items-center gap-2 px-1 text-xs text-muted-foreground">
885 <InfoIcon className="w-3.5 h-3.5 shrink-0" />
886 <span className="flex-1">
887 Requires the{" "}
888 <ExternalLink
889 to={cat.skill.url}
890 className="underline font-medium text-foreground hover:text-primary transition-colors"
891 >
892 {cat.skill.skillName}
893 </ExternalLink>{" "}
894 — ask your agent:{" "}
895 <em>“{cat.skill.prompt}”</em>
896 </span>
897 </div>
898 )}
899 </TabsContent>
900 ))}
901 </div>
902 </div>
903 </Tabs>
904 );
905 }
906