SkateSpots/src/components/SasquatchActionCard.tsx
shakespeare.diy 5655fbc976 Sasquatch Pro Phase 2: action drafting (draft → you confirm)
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>
2026-07-29 23:41:11 +07:00

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>
);
}