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 &rarr; Connectors</li>
 122          <li>Add custom connector</li>
 123          <li>Enter &ldquo;Alby&rdquo; 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">&gt; </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">&gt; </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">&gt; </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                      &quot;Buy a $15 DoorDash gift card&quot; — 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                      &mdash; ask your agent:{" "}
 895                      <em>&ldquo;{cat.skill.prompt}&rdquo;</em>
 896                    </span>
 897                  </div>
 898                )}
 899              </TabsContent>
 900            ))}
 901          </div>
 902        </div>
 903      </Tabs>
 904    );
 905  }
 906