Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,36 @@ export function ConnectionPicker({
const advancedLock = tierLock(active, "advanced");
const showMaxUpgrade =
active?.auth_method === "deployment" && Boolean(advancedLock);
const chatGPTUpgrade = offers.find(
(offer) =>
offer.provider_family === "openai" &&
offer.auth_method === "chatgpt_oauth" &&
offer.state === "locked" &&
!offer.selectable &&
!offer.credential_id &&
offer.lock_reason &&
offer.unlock_href === "/settings/billing",
);
const connectionOffers = offers.filter((offer) => offer !== chatGPTUpgrade);
const inlineUpgrade = showTiers && showMaxUpgrade;
const upgradeHref =
(inlineUpgrade && advancedLock?.href) || chatGPTUpgrade?.unlock_href;
const upgradeCard = upgradeHref ? (
<MaxUpgradeCard
href={upgradeHref}
advanced={
inlineUpgrade && advancedLock
? {
label: tierLabel(active, "advanced"),
name: tierName(active, "advanced"),
model: tierModel(active, "advanced"),
reason: advancedLock.reason,
}
: undefined
}
chatGPTReason={chatGPTUpgrade?.lock_reason ?? undefined}
/>
) : null;

if (isLoading && offers.length === 0) return null;

Expand Down Expand Up @@ -132,7 +162,7 @@ export function ConnectionPicker({
return null;
}

const connectionOptions = offers.map((offer) => ({
const connectionOptions = connectionOffers.map((offer) => ({
value: offer.offer_id,
disabled: !isSelectable(offer),
}));
Expand Down Expand Up @@ -161,7 +191,7 @@ export function ConnectionPicker({
// remains visible.
const showsConnections =
(!connectionLocked || onlyOfferIsLocked) &&
(offers.length > 1 || onlyOfferIsLocked);
(connectionOffers.length > 1 || Boolean(connectionOffers[0]?.lock_reason));

// Naming only the tier, the chip folds down to its glyph among the other
// quiet icons on the composer's right; the tier and its model wait in the
Expand Down Expand Up @@ -226,7 +256,7 @@ export function ConnectionPicker({
align="end"
className={cn(
"max-h-[var(--radix-popover-content-available-height)] w-[24rem] max-w-[calc(100vw-2rem)] overflow-y-auto rounded-2xl border-zinc-200 bg-[#F9F9FA] p-3 pt-4 text-zinc-900 shadow-lg",
showMaxUpgrade && "bg-white",
upgradeCard && "bg-popover text-popover-foreground",
)}
>
{showsConnections && (
Expand All @@ -250,14 +280,14 @@ export function ConnectionPicker({
);
if (to === null) return;
event.preventDefault();
const target = offers.find((o) => o.offer_id === to);
const target = connectionOffers.find((o) => o.offer_id === to);
if (target) chooseConnection(target);
event.currentTarget
.querySelector<HTMLElement>(`[data-offer="${to}"]`)
?.focus();
}}
>
{offers.map((offer) => (
{connectionOffers.map((offer) => (
<ChoiceRow
key={offer.offer_id}
offerId={offer.offer_id}
Expand Down Expand Up @@ -308,17 +338,7 @@ export function ConnectionPicker({
<TierToggle
value={tier}
onSelect={setTier}
advancedUpgrade={
showMaxUpgrade && advancedLock?.href ? (
<MaxUpgradeCard
label={tierLabel(active, "advanced")}
name={tierName(active, "advanced")}
model={tierModel(active, "advanced")}
reason={advancedLock.reason}
href={advancedLock.href}
/>
) : undefined
}
advancedUpgrade={inlineUpgrade ? upgradeCard : undefined}
segments={TIERS.map((candidate) => ({
tier: candidate,
label: tierLabel(active, candidate),
Expand All @@ -331,6 +351,8 @@ export function ConnectionPicker({
</>
)}

{!inlineUpgrade && upgradeCard}

{canConnectChatGPT && (
<div
className={cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,41 @@
import {
AiBrain01Icon,
ArrowRight02Icon,
LockIcon,
} from "@hugeicons/core-free-icons";
import { AiBrain01Icon, ArrowRight02Icon } from "@hugeicons/core-free-icons";

import { Button } from "@/components/atoms/Button/Button";
import { Icon } from "@/components/atoms/Icon/Icon";
import { IntegrationLogo } from "@/components/molecules/IntegrationLogo/IntegrationLogo";

interface Props {
interface AdvancedTier {
label: string;
name: string;
model?: string | null;
reason: string;
}

interface Props {
advanced?: AdvancedTier;
chatGPTReason?: string;
href: string;
}

export function MaxUpgradeCard({ label, name, model, reason, href }: Props) {
export function MaxUpgradeCard({ advanced, chatGPTReason, href }: Props) {
return (
<div className="mt-3 rounded-xl border border-purple-200/40 bg-purple-50 p-4">
<div
role="radio"
aria-checked={false}
aria-disabled="true"
aria-label={`${label}${reason}`}
tabIndex={-1}
className="flex items-start gap-3"
>
<span className="flex size-8 flex-none items-center justify-center rounded-lg border border-purple-200/60 bg-white text-purple-600">
<Icon icon={AiBrain01Icon} size={18} aria-hidden />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-zinc-900">
{name}
</span>
{model && (
<span className="mt-0.5 block break-words text-xs text-zinc-600">
{model}
</span>
)}
</span>
<span className="inline-flex items-center gap-1 rounded bg-white px-1.5 py-1 text-[10px] font-semibold tracking-wide text-purple-700">
<Icon icon={LockIcon} size={10} aria-hidden />
<div className="mt-3 rounded-xl border border-border bg-muted/70 p-4">
<p className="mb-4 flex items-center gap-2 text-xs leading-5 text-muted-foreground">
Included with AutoGPT
<span className="rounded bg-purple-50 px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-purple-700">
MAX
</span>
</div>
<p className="mb-2 mt-5 text-lg font-medium leading-snug tracking-tight text-zinc-900">
Unlock {name} with Max.
</p>
<p className="text-xs leading-relaxed text-zinc-600">{reason}</p>
<div className="space-y-5">
{advanced && <AdvancedBenefit advanced={advanced} />}
{chatGPTReason && <ChatGPTBenefit reason={chatGPTReason} />}
</div>
<Button
as="NextLink"
href={href}
variant="primary"
size="small"
className="mt-5 h-11 w-full rounded-lg border-purple-600 bg-purple-600 text-white hover:border-purple-700 hover:bg-purple-700"
className="mt-5 h-[42px] w-full rounded-lg border-primary bg-primary text-[13px] text-primary-foreground hover:border-primary/90 hover:bg-primary/90"
data-fast-goal="subscription_upgrade_intent"
data-fast-goal-surface="model_picker"
>
Expand All @@ -63,3 +45,53 @@ export function MaxUpgradeCard({ label, name, model, reason, href }: Props) {
</div>
);
}

function AdvancedBenefit({ advanced }: { advanced: AdvancedTier }) {
return (
<div
role="radio"
aria-checked={false}
aria-disabled="true"
aria-label={`${advanced.label}${advanced.reason}`}
tabIndex={-1}
className="flex items-start gap-3"
>
<span className="flex h-9 w-8 flex-none items-center justify-center text-foreground">
<Icon icon={AiBrain01Icon} size={20} aria-hidden />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-foreground">
{advanced.name}
</span>
{advanced.model && (
<span className="mt-0.5 block break-words text-xs leading-relaxed text-muted-foreground">
{advanced.model}
</span>
)}
</span>
</div>
);
}

function ChatGPTBenefit({ reason }: { reason: string }) {
return (
<div
role="group"
aria-label={`Connect ChatGPT — ${reason}`}
className="flex items-start gap-3"
>
<span className="flex h-9 w-8 flex-none items-center justify-center">
<IntegrationLogo provider="openai" size={20} />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-foreground">
Connect ChatGPT
</span>
<span className="mt-0.5 block text-xs leading-relaxed text-muted-foreground">
<span className="block">Use your existing ChatGPT plan.</span>
<span className="block">Save AutoGPT credits on chats.</span>
</span>
</span>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useGetV2ListChatConnections } from "@/app/api/__generated__/endpoints/chat/chat";
import { useGetV1ListProviders } from "@/app/api/__generated__/endpoints/integrations/integrations";
import type { AIConnectionOffer } from "@/app/api/__generated__/models/aIConnectionOffer";
import { useOAuthConnect } from "@/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/DetailView/useOAuthConnect";

Expand All @@ -27,6 +28,17 @@ export function useConnectionPicker() {
const offers = visibleOffers(
query.data?.status === 200 ? query.data.data.offers : undefined,
);
const needsChatGPTConnection =
query.data?.status === 200 &&
offers.length > 0 &&
offers.every((offer) => offer.provider_family !== "openai");
const providersQuery = useGetV1ListProviders({
query: {
enabled: needsChatGPTConnection,
refetchOnWindowFocus: true,
staleTime: 0,
},
});
// Only ever consider what can actually run: a locked offer is listed to
// explain itself, never to be landed on.
const choosable = offers.filter(isSelectable);
Expand Down Expand Up @@ -72,19 +84,13 @@ export function useConnectionPicker() {
hasConnectionChoice: choosable.length > 1,
connectChatGPT,
isConnecting,
// A ChatGPT offer of any kind means the server has already had its say:
// either it is linked, or it is locked behind a plan and connecting is not
// the next step. Its absence is the one case where linking is what the
// user is missing, and the picker is where they are asking about it.
//
// No offers at all is not that case. The picker has no popover to put the
// row in then -- it collapses to a link to Settings, which is both where
// the user has to go to fix a deployment with no routes at all and a way
// in for every provider rather than this one.
// The upsell flag can hide a locked offer. Only provider discovery can
// confirm that an unconnected account is eligible to connect.
canConnectChatGPT:
query.data?.status === 200 &&
offers.length > 0 &&
offers.every((offer) => offer.provider_family !== "openai"),
needsChatGPTConnection &&
providersQuery.isSuccess &&
providersQuery.data.status === 200 &&
providersQuery.data.data.some((provider) => provider.name === "codex"),
isLoading: query.isLoading,
// React Query can retain the last successful offers while a background
// refetch fails. Keep that usable snapshot visible; the error state is
Expand Down
Loading
Loading