Sasquatch can now DRAFT actions from natural language and publish them only after the user reviews & confirms — it never publishes on its own. - useSasquatchActions: publishes confirmed drafts via the app's normal publish path. Supports post_status (NIP-38 kind 30315, auto-expiring) and list_gear (NIP-99 kind 30402, skateboarding-tagged), reusing the existing event schemas. - SasquatchActionCard: inline "review & confirm" card with editable key fields (status text / listing title, price, condition), Publish + Cancel, and done/cancelled states. Nothing is signed until Publish. - SasquatchGuide: new draft_action tool (Pro-gated). The AI emits a draft; a card renders in-chat; confirmDraft signs & publishes. Non-Pro users get an upgrade nudge. Draft lifecycle tracked per message. - Prompt teaches the model to use draft_action for "post/sell/list/set my status" requests and to fill gear fields from the stash first. Examples now working (Pro): "post that I'm skating Southbank for 3 hours" · "list my Spitfire wheels for 8000 sats". Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
|
* SasquatchActionCard — the "review & confirm" card for a drafted action.
|
|
*
|
|
* Sasquatch proposes a draft; the user can tweak the key field(s), then Publish
|
|
* or Cancel. Nothing is signed or published until Publish is tapped.
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Loader2, Check, X, Wand2 } from 'lucide-react';
|
|
import {
|
|
summarizeDraft,
|
|
type SasquatchDraft,
|
|
} from '@/hooks/useSasquatchActions';
|
|
|
|
interface Props {
|
|
draft: SasquatchDraft;
|
|
onPublish: (draft: SasquatchDraft) => Promise<void>;
|
|
onCancel: () => void;
|
|
/** Set once resolved so the card locks into a done/cancelled state. */
|
|
status: 'pending' | 'publishing' | 'done' | 'cancelled';
|
|
resultLabel?: string;
|
|
}
|
|
|
|
export function SasquatchActionCard({ draft, onPublish, onCancel, status, resultLabel }: Props) {
|
|
const [edited, setEdited] = useState<SasquatchDraft>(draft);
|
|
const summary = summarizeDraft(edited);
|
|
|
|
if (status === 'done') {
|
|
return (
|
|
<div className="rounded-xl border border-[#ccff00]/30 bg-[#ccff00]/10 p-3 text-xs text-[#ccff00] flex items-center gap-2">
|
|
<Check className="h-4 w-4 shrink-0" /> {resultLabel ?? 'Done!'}
|
|
</div>
|
|
);
|
|
}
|
|
if (status === 'cancelled') {
|
|
return (
|
|
<div className="rounded-xl border border-white/10 bg-white/[0.03] p-3 text-xs text-gray-500 flex items-center gap-2">
|
|
<X className="h-4 w-4 shrink-0" /> Cancelled — nothing was published.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const busy = status === 'publishing';
|
|
|
|
return (
|
|
<div className="rounded-xl border border-red-500/30 bg-red-500/[0.06] p-3 space-y-2.5">
|
|
<div className="flex items-center gap-1.5 text-[10px] font-black uppercase tracking-wider text-red-400">
|
|
<Wand2 className="h-3 w-3" /> Draft — review & confirm
|
|
</div>
|
|
|
|
<p className="text-sm font-bold text-white">{summary.title}</p>
|
|
|
|
{/* Editable key field(s) */}
|
|
{edited.type === 'post_status' ? (
|
|
<Input
|
|
value={edited.text}
|
|
onChange={(e) => setEdited({ ...edited, text: e.target.value })}
|
|
disabled={busy}
|
|
className="h-9 bg-black/40 border-white/10 text-white text-sm"
|
|
placeholder="Status text"
|
|
/>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Input
|
|
value={edited.title}
|
|
onChange={(e) => setEdited({ ...edited, title: e.target.value })}
|
|
disabled={busy}
|
|
className="col-span-2 h-9 bg-black/40 border-white/10 text-white text-sm"
|
|
placeholder="Title"
|
|
/>
|
|
<Input
|
|
value={edited.price ?? ''}
|
|
onChange={(e) => setEdited({ ...edited, price: e.target.value ? Number(e.target.value) : undefined })}
|
|
disabled={busy}
|
|
type="number"
|
|
inputMode="numeric"
|
|
className="h-9 bg-black/40 border-white/10 text-white text-sm"
|
|
placeholder="Price (sats)"
|
|
/>
|
|
<Input
|
|
value={edited.condition ?? ''}
|
|
onChange={(e) => setEdited({ ...edited, condition: e.target.value })}
|
|
disabled={busy}
|
|
className="h-9 bg-black/40 border-white/10 text-white text-sm"
|
|
placeholder="Condition"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Read-only summary lines */}
|
|
<ul className="text-[11px] text-gray-400 space-y-0.5">
|
|
{summary.lines.map((l, i) => (
|
|
<li key={i} className="truncate">• {l}</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="flex gap-2 pt-0.5">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 gap-1.5 border-white/15 text-gray-300 h-8"
|
|
onClick={onCancel}
|
|
disabled={busy}
|
|
>
|
|
<X className="h-3.5 w-3.5" /> Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
className="flex-1 gap-1.5 bg-red-500 hover:bg-red-600 font-bold h-8"
|
|
onClick={() => onPublish(edited)}
|
|
disabled={busy}
|
|
>
|
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
|
|
{summary.cta}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|