Improve links/buttons for AI errors (#1524)

drive-by: update links for pro banners

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Revamps ChatErrorBox with styled CTA buttons, UTM-tracked links,
broader error matching, and adds default help CTAs; updates Pro banners
to point to /pro URLs.
> 
> - **Chat**:
> - **Error handling/UI**: Expands rate-limit matching to include
`"Provider returned error"` and appends default CTAs on generic errors.
> - **CTAs**: Replaces underlined links with styled buttons via
`ExternalLink` (supports `primary|secondary` variants and icons), and
adds upgrade/docs buttons across error states.
> - **Links**: Adds UTM parameters to Pro/Docs links; updates exceeded
budget CTA to `"Reload or upgrade your subscription"` with new
subscription URL.
> - **Pro Banners**:
> - Adjusts banner click-through URLs in `AiAccessBanner`,
`SmartContextBanner`, and `TurboBanner` to `https://www.dyad.sh/pro?...`
(removes `#ai`).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2111954ac9a98dd66f5b3b9e628270ac19713a2d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Will Chen
2025-10-13 17:05:04 -07:00
committed by GitHub
parent ffa4c3ad01
commit 7acbe73c73
2 changed files with 71 additions and 19 deletions

View File

@@ -85,7 +85,7 @@ export function AiAccessBanner() {
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-white via-indigo-50 to-sky-100 dark:from-indigo-700 dark:via-indigo-700 dark:to-indigo-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-black/5 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-white via-indigo-50 to-sky-100 dark:from-indigo-700 dark:via-indigo-700 dark:to-indigo-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-black/5 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
onClick={() => { onClick={() => {
IpcClient.getInstance().openExternalUrl( IpcClient.getInstance().openExternalUrl(
"https://www.dyad.sh/pro#ai?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-ai-access", "https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-ai-access",
); );
}} }}
> >
@@ -151,7 +151,7 @@ export function SmartContextBanner() {
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-200 dark:from-emerald-700 dark:via-emerald-700 dark:to-emerald-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-emerald-900/10 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-200 dark:from-emerald-700 dark:via-emerald-700 dark:to-emerald-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-emerald-900/10 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
onClick={() => { onClick={() => {
IpcClient.getInstance().openExternalUrl( IpcClient.getInstance().openExternalUrl(
"https://www.dyad.sh/pro#ai?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-smart-context", "https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-smart-context",
); );
}} }}
> >
@@ -192,7 +192,7 @@ export function TurboBanner() {
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-rose-50 via-rose-100 to-rose-200 dark:from-rose-800 dark:via-fuchsia-800 dark:to-rose-800 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-rose-900/10 dark:ring-white/5 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-rose-50 via-rose-100 to-rose-200 dark:from-rose-800 dark:via-fuchsia-800 dark:to-rose-800 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-rose-900/10 dark:ring-white/5 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
onClick={() => { onClick={() => {
IpcClient.getInstance().openExternalUrl( IpcClient.getInstance().openExternalUrl(
"https://www.dyad.sh/pro#ai?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-turbo", "https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=in-app-banner-turbo",
); );
}} }}
> >

View File

@@ -1,5 +1,9 @@
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { X } from "lucide-react"; import {
X,
ExternalLink as ExternalLinkIcon,
CircleArrowUp,
} from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -17,7 +21,10 @@ export function ChatErrorBox({
<ChatErrorContainer onDismiss={onDismiss}> <ChatErrorContainer onDismiss={onDismiss}>
{error} {error}
<span className="ml-1"> <span className="ml-1">
<ExternalLink href="https://dyad.sh/pro"> <ExternalLink
href="https://dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=free-quota-error"
variant="primary"
>
Access with Dyad Pro Access with Dyad Pro
</ExternalLink> </ExternalLink>
</span>{" "} </span>{" "}
@@ -30,22 +37,24 @@ export function ChatErrorBox({
// because it also includes this URL in the error message // because it also includes this URL in the error message
if ( if (
error.includes("Resource has been exhausted") || error.includes("Resource has been exhausted") ||
error.includes("https://ai.google.dev/gemini-api/docs/rate-limits") error.includes("https://ai.google.dev/gemini-api/docs/rate-limits") ||
error.includes("Provider returned error")
) { ) {
return ( return (
<ChatErrorContainer onDismiss={onDismiss}> <ChatErrorContainer onDismiss={onDismiss}>
{error} {error}
<span className="ml-1"> <div className="mt-2 space-y-2 space-x-2">
<ExternalLink href="https://dyad.sh/pro"> <ExternalLink
href="https://dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=rate-limit-error"
variant="primary"
>
Upgrade to Dyad Pro Upgrade to Dyad Pro
</ExternalLink> </ExternalLink>
</span>{" "}
or read the
<span className="ml-1">
<ExternalLink href="https://dyad.sh/docs/help/ai-rate-limit"> <ExternalLink href="https://dyad.sh/docs/help/ai-rate-limit">
Rate limit troubleshooting guide. Troubleshooting guide
</ExternalLink> </ExternalLink>
</span> </div>
</ChatErrorContainer> </ChatErrorContainer>
); );
} }
@@ -55,7 +64,10 @@ export function ChatErrorBox({
<ChatInfoContainer onDismiss={onDismiss}> <ChatInfoContainer onDismiss={onDismiss}>
<span> <span>
Looks like you don't have a valid Dyad Pro key.{" "} Looks like you don't have a valid Dyad Pro key.{" "}
<ExternalLink href="https://dyad.sh/pro"> <ExternalLink
href="https://dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=invalid-pro-key-error"
variant="primary"
>
Upgrade to Dyad Pro Upgrade to Dyad Pro
</ExternalLink>{" "} </ExternalLink>{" "}
today. today.
@@ -68,8 +80,11 @@ export function ChatErrorBox({
<ChatInfoContainer onDismiss={onDismiss}> <ChatInfoContainer onDismiss={onDismiss}>
<span> <span>
You have used all of your Dyad AI credits this month.{" "} You have used all of your Dyad AI credits this month.{" "}
<ExternalLink href="https://academy.dyad.sh/subscription"> <ExternalLink
Upgrade to Dyad Max href="https://academy.dyad.sh/subscription?utm_source=dyad-app&utm_medium=app&utm_campaign=exceeded-budget-error"
variant="primary"
>
Reload or upgrade your subscription
</ExternalLink>{" "} </ExternalLink>{" "}
and get more AI credits and get more AI credits
</span> </span>
@@ -80,22 +95,59 @@ export function ChatErrorBox({
if (error.includes("Fallbacks=")) { if (error.includes("Fallbacks=")) {
error = error.split("Fallbacks=")[0]; error = error.split("Fallbacks=")[0];
} }
return <ChatErrorContainer onDismiss={onDismiss}>{error}</ChatErrorContainer>; return (
<ChatErrorContainer onDismiss={onDismiss}>
{error}
<div className="mt-2 space-y-2 space-x-2">
<ExternalLink
href="https://dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=general-error"
variant="primary"
>
Upgrade to Dyad Pro
</ExternalLink>
<ExternalLink href="https://www.dyad.sh/docs/faq">
Read docs
</ExternalLink>
</div>
</ChatErrorContainer>
);
} }
function ExternalLink({ function ExternalLink({
href, href,
children, children,
variant = "secondary",
icon,
}: { }: {
href: string; href: string;
children: React.ReactNode; children: React.ReactNode;
variant?: "primary" | "secondary";
icon?: React.ReactNode;
}) { }) {
const baseClasses =
"cursor-pointer inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium shadow-sm focus:outline-none focus:ring-2";
const primaryClasses =
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500";
const secondaryClasses =
"bg-blue-50 text-blue-700 border border-blue-200 hover:bg-blue-100 hover:border-blue-300 focus:ring-blue-200";
const iconElement =
icon ??
(variant === "primary" ? (
<CircleArrowUp size={18} />
) : (
<ExternalLinkIcon size={14} />
));
return ( return (
<a <a
className="underline cursor-pointer text-blue-500 hover:text-blue-700" className={`${baseClasses} ${
variant === "primary" ? primaryClasses : secondaryClasses
}`}
onClick={() => IpcClient.getInstance().openExternalUrl(href)} onClick={() => IpcClient.getInstance().openExternalUrl(href)}
> >
{children} <span>{children}</span>
{iconElement}
</a> </a>
); );
} }