mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): send minted agent cards to recipients via normal DM share path
Closes the v1 scope gap Wren flagged: the mint dialog only offered Save. Card preview now includes PersonaShareRecipients + a Send action that routes through useSnapshotSendController — the exact guarded encode → upload (uploadMediaBytes) → send pipeline agent snapshots use, with the same eligibility checkpoints and DM-safety gating. No card-special relay path, per the kickoff contract. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
parent
eba5ab72bb
commit
19b449f010
@@ -1,14 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { Download, RefreshCw, Sparkles } from "lucide-react";
|
||||
import { Download, RefreshCw, Send, Sparkles } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
useOpenDmMutation,
|
||||
useUpsertCachedChannel,
|
||||
} from "@/features/channels/hooks";
|
||||
import {
|
||||
mintAgentCard,
|
||||
NO_OPENAI_KEY_PREFIX,
|
||||
saveAgentCard,
|
||||
type MintedAgentCard,
|
||||
} from "@/shared/api/tauriPersonas";
|
||||
import type { UserSearchResult } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,6 +24,14 @@ import {
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
|
||||
import { PersonaShareRecipients } from "./PersonaShareRecipients";
|
||||
import { useSnapshotSendController } from "./useSnapshotSendController";
|
||||
|
||||
/** Decode base64 card bytes into the number[] shape the upload path expects. */
|
||||
function cardBytesFromBase64(b64: string): number[] {
|
||||
return Array.from(atob(b64), (char) => char.charCodeAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint-a-trading-card dialog: optional style notes → one long Rust-side
|
||||
* Responses API call → preview with reroll → save as `.agent.png`.
|
||||
@@ -39,6 +52,11 @@ export function AgentCardMintDialog({
|
||||
}) {
|
||||
const [styleNotes, setStyleNotes] = React.useState("");
|
||||
const [card, setCard] = React.useState<MintedAgentCard | null>(null);
|
||||
const [recipients, setRecipients] = React.useState<UserSearchResult[]>([]);
|
||||
|
||||
const sendController = useSnapshotSendController(card !== null);
|
||||
const openDmMutation = useOpenDmMutation();
|
||||
const upsertCachedChannel = useUpsertCachedChannel();
|
||||
|
||||
const mintMutation = useMutation({
|
||||
mutationFn: () => mintAgentCard(agentId, styleNotes.trim() || undefined),
|
||||
@@ -69,9 +87,38 @@ export function AgentCardMintDialog({
|
||||
});
|
||||
|
||||
const isMinting = mintMutation.isPending;
|
||||
const isSending =
|
||||
sendController.state.phase !== "idle" &&
|
||||
sendController.state.phase !== "done" &&
|
||||
sendController.state.phase !== "error";
|
||||
const isBusy = isMinting || saveMutation.isPending || isSending;
|
||||
|
||||
async function sendToRecipients(minted: MintedAgentCard) {
|
||||
if (recipients.length === 0) return;
|
||||
const sent = await sendController.beginSend(
|
||||
async () => ({
|
||||
fileBytes: cardBytesFromBase64(minted.cardPngBase64),
|
||||
fileName: minted.fileName,
|
||||
}),
|
||||
async () => {
|
||||
const directMessage = await openDmMutation.mutateAsync({
|
||||
pubkeys: recipients.map((recipient) => recipient.pubkey),
|
||||
});
|
||||
await upsertCachedChannel(directMessage);
|
||||
return directMessage.id;
|
||||
},
|
||||
agentName,
|
||||
);
|
||||
if (sent) {
|
||||
toast.success(`Sent ${agentName}'s card.`);
|
||||
onOpenChange(false);
|
||||
} else if (sent === false) {
|
||||
toast.error("Couldn’t send the card. Try again.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={(open) => !isMinting && onOpenChange(open)} open>
|
||||
<Dialog onOpenChange={(open) => !isBusy && onOpenChange(open)} open>
|
||||
<DialogContent className="max-w-md" data-testid="agent-card-mint-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
@@ -93,9 +140,21 @@ export function AgentCardMintDialog({
|
||||
data-testid="agent-card-preview"
|
||||
src={`data:image/png;base64,${card.cardPngBase64}`}
|
||||
/>
|
||||
<PersonaShareRecipients
|
||||
disabled={isBusy || !sendController.isDmSafetyReady}
|
||||
excludedPubkeys={
|
||||
sendController.relaySelfPubkey
|
||||
? [sendController.relaySelfPubkey]
|
||||
: []
|
||||
}
|
||||
onSelectionChange={setRecipients}
|
||||
open
|
||||
selectedUsers={recipients}
|
||||
testIdPrefix="agent-card-share"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
disabled={isMinting || saveMutation.isPending}
|
||||
disabled={isBusy}
|
||||
onClick={() => mintMutation.mutate()}
|
||||
variant="outline"
|
||||
>
|
||||
@@ -103,13 +162,24 @@ export function AgentCardMintDialog({
|
||||
{isMinting ? "Rerolling…" : "Reroll"}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isMinting || saveMutation.isPending}
|
||||
disabled={isBusy}
|
||||
onClick={() => saveMutation.mutate(card)}
|
||||
data-testid="agent-card-save"
|
||||
variant={recipients.length > 0 ? "outline" : "default"}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Save card
|
||||
</Button>
|
||||
{recipients.length > 0 ? (
|
||||
<Button
|
||||
disabled={isBusy}
|
||||
onClick={() => void sendToRecipients(card)}
|
||||
data-testid="agent-card-send"
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{isSending ? "Sending…" : "Send"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user