diff --git a/desktop/src/features/agents/ui/AgentCardMintDialog.tsx b/desktop/src/features/agents/ui/AgentCardMintDialog.tsx new file mode 100644 index 000000000..6e406e18f --- /dev/null +++ b/desktop/src/features/agents/ui/AgentCardMintDialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import { Download, RefreshCw, Sparkles } from "lucide-react"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { + mintAgentCard, + NO_OPENAI_KEY_PREFIX, + saveAgentCard, + type MintedAgentCard, +} from "@/shared/api/tauriPersonas"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { Textarea } from "@/shared/ui/textarea"; + +/** + * Mint-a-trading-card dialog: optional style notes → one long Rust-side + * Responses API call → preview with reroll → save as `.agent.png`. + * + * The saved PNG carries the agent's `buzz_agent_snapshot` chunk, so sharing + * the card shares an importable agent (config only — never memory, never + * identity). All snapshot construction and verification happens in Rust. + */ +export function AgentCardMintDialog({ + agentId, + agentName, + onOpenChange, +}: { + /** Instance pubkey or definition slug — same resolution as snapshot export. */ + agentId: string; + agentName: string; + onOpenChange: (open: boolean) => void; +}) { + const [styleNotes, setStyleNotes] = React.useState(""); + const [card, setCard] = React.useState(null); + + const mintMutation = useMutation({ + mutationFn: () => mintAgentCard(agentId, styleNotes.trim() || undefined), + onSuccess: (minted) => setCard(minted), + onError: (error: Error) => { + if (error.message.startsWith(NO_OPENAI_KEY_PREFIX)) { + toast.error( + "No OpenAI API key found. Add OPENAI_API_KEY in the agent's environment variables or global agent settings.", + ); + } else { + toast.error(error.message); + } + }, + }); + + const saveMutation = useMutation({ + mutationFn: (minted: MintedAgentCard) => + saveAgentCard(minted.cardPngBase64, minted.fileName), + onSuccess: (saved) => { + if (saved) { + toast.success( + `Saved ${agentName}'s card. Share it — the card IS the agent.`, + ); + onOpenChange(false); + } + }, + onError: (error: Error) => toast.error(error.message), + }); + + const isMinting = mintMutation.isPending; + + return ( + !isMinting && onOpenChange(open)} open> + + + + + {card ? `${agentName}'s card` : `Create ${agentName}'s card`} + + + {card + ? "The card carries the agent — anyone who imports this PNG gets a working copy (config only, fresh identity, no memories)." + : "Mint a collectible trading card that doubles as a shareable, importable copy of this agent."} + + + + {card ? ( +
+ {`${agentName} +
+ + +
+
+ ) : ( +
+