import type { ReactNode } from "react"; /* -------------------------------------------------------------------------- */ /* Result parsing */ /* -------------------------------------------------------------------------- */ /** * `useRenderTool` hands `result` back as a JSON string once the tool finishes. * This helper parses it (and tolerates an already-parsed object, just in case). */ export function parseResult(result: unknown): T | undefined { if (result == null) return undefined; if (typeof result === "object") return result as T; if (typeof result === "string") { if (result.length === 0) return undefined; try { return JSON.parse(result) as T; } catch { return undefined; } } return undefined; } export type ArcadeResult = | { authorizationRequired: true; provider: string; toolName: string; authUrl: string; } | { authorizationRequired: false; provider: string; toolName: string; output: unknown; } | { error: string; toolName: string }; export type NewsStory = { source?: string; title?: string; link?: string }; export type Email = { from?: string; sender?: string; subject?: string; snippet?: string; body?: string; date?: string; }; export function extractNews(output: unknown): NewsStory[] { if (Array.isArray(output)) return output as NewsStory[]; const value = output as { news_results?: NewsStory[] } | null; return value?.news_results ?? []; } export function extractEmails(output: unknown): Email[] { if (Array.isArray(output)) return output as Email[]; const value = output as { emails?: Email[]; messages?: Email[] } | null; return value?.emails ?? value?.messages ?? []; } /* -------------------------------------------------------------------------- */ /* Icons (inline, no extra deps) */ /* -------------------------------------------------------------------------- */ type IconProps = { className?: string }; const Icon = ({ children, className, }: { children: ReactNode; className?: string; }) => ( ); const LockIcon = (p: IconProps) => ( ); const CheckIcon = (p: IconProps) => ( ); const InboxIcon = (p: IconProps) => ( ); const NewsIcon = (p: IconProps) => ( ); const AlertIcon = (p: IconProps) => ( ); const SparkIcon = (p: IconProps) => ( ); const ArrowIcon = (p: IconProps) => ( ); /* -------------------------------------------------------------------------- */ /* Card primitives */ /* -------------------------------------------------------------------------- */ type Accent = "violet" | "emerald" | "sky" | "zinc" | "red"; const accentRing: Record = { violet: "border-violet-200 bg-violet-50/60", emerald: "border-emerald-200 bg-emerald-50/60", sky: "border-sky-200 bg-sky-50/60", zinc: "border-zinc-200 bg-zinc-50/60", red: "border-red-200 bg-red-50/60", }; const accentChip: Record = { violet: "bg-violet-600 text-white", emerald: "bg-emerald-600 text-white", sky: "bg-sky-600 text-white", zinc: "bg-zinc-700 text-white", red: "bg-red-600 text-white", }; function ToolCard({ accent, icon, title, badge, children, }: { accent: Accent; icon: ReactNode; title: string; badge?: string; children?: ReactNode; }) { return (
{icon}

{title}

{badge && ( {badge} )}
{children &&
{children}
}
); } /* -------------------------------------------------------------------------- */ /* Cards */ /* -------------------------------------------------------------------------- */ export function LoadingCard({ label }: { label: string }) { return ( } title={label} >
); } /** * Only ever link to a real http(s) URL, never an attacker-controlled scheme * (javascript:, data:, etc.). Tool results flow from external services through the * model into href attributes, so every link (auth URLs AND news links) goes through this. */ function safeHttpUrl(url: string | undefined): string | undefined { if (!url) return undefined; try { const parsed = new URL(url); return parsed.protocol === "https:" || parsed.protocol === "http:" ? url : undefined; } catch { return undefined; } } /** The star of the cookbook: rendered when Arcade needs the user to connect. */ export function AuthorizationCard({ provider, authUrl, }: { provider: string; authUrl: string; }) { const href = safeHttpUrl(authUrl); return ( } title={`Connect ${provider}`} badge="Authorization" >

Arcade needs you to authorize{" "} {provider} once. Your credentials are vaulted by Arcade and never shared with the model.

{href ? ( Connect {provider} ) : ( Authorization link unavailable. Please try again. )} Opens in a new tab. Come back when you’re done and say “continue”.
); } export function EmailSentCard({ recipient, subject, }: { recipient?: string; subject?: string; }) { return ( } title="Email sent" badge="Gmail" >
To
{recipient || "-"}
Subject
{subject || "-"}
); } export function EmailListCard({ emails }: { emails: Email[] }) { return ( } title={`Inbox: ${emails.length} ${emails.length === 1 ? "email" : "emails"}`} badge="Gmail" > {emails.length === 0 ? (

No emails found.

) : (
    {emails.slice(0, 8).map((email, i) => (
  • {email.subject || "(no subject)"}

    {email.from || email.sender || "Unknown sender"} {email.snippet ? ( {` ${email.snippet}`} ) : null}

  • ))}
)}
); } export function NewsCard({ keywords, stories, }: { keywords?: string; stories: NewsStory[]; }) { return ( } title={keywords ? `News: “${keywords}”` : "News results"} badge="Google News" > {stories.length === 0 ? (

No stories found.

) : (
    {stories.slice(0, 6).map((story, i) => { const href = safeHttpUrl(story.link); const inner = ( <>

    {story.title || "Untitled"}

    {story.source && (

    {story.source}

    )} ); return (
  • {href ? ( {inner} ) : (
    {inner}
    )}
  • ); })}
)}
); } export function ErrorCard({ message }: { message: string }) { return ( } title="Something went wrong" badge="Error" >

{message}

); } export function GenericToolCard({ name, done, }: { name: string; done: boolean; }) { return ( } title={name} badge={done ? "Done" : "Running…"} /> ); }