feat(frontend): Use USD value instead of cents credit value (#9423)

The use of credit points (equivalent to cents) can cause confussion, the
scope of this PR is removing the use of it in the UI and using USD value
instead.

### Changes 🏗️

Show US values on:
* credit page
* block cost
* balance button in the navbar

<img width="1440" alt="image"
src="https://github.com/user-attachments/assets/2b6a18b0-f89d-48bf-84bd-0ea6aa9182f7"
/>

<img width="1440" alt="image"
src="https://github.com/user-attachments/assets/7545b496-0e7c-49ea-afc1-a3d1fd3289fb"
/>



### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>
pull/9427/head
Zamil Majdy 2025-02-05 11:40:57 +01:00 committed by GitHub
parent 533d120e98
commit 243122311c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 58 additions and 272 deletions

View File

@ -328,7 +328,7 @@ class UserCredit(UserCreditBase):
# Auto top-up if balance just went below threshold due to this transaction.
auto_top_up = await get_auto_top_up(user_id)
if balance < auto_top_up.threshold <= balance - cost:
if balance < auto_top_up.threshold <= balance + cost:
try:
await self.top_up_credits(user_id=user_id, amount=auto_top_up.amount)
except Exception as e:

View File

@ -23,6 +23,7 @@ export default function CreditsPage() {
updateAutoTopUpConfig,
transactionHistory,
fetchTransactionHistory,
formatCredits,
} = useCredits();
const router = useRouter();
const searchParams = useSearchParams();
@ -59,7 +60,8 @@ export default function CreditsPage() {
const submitTopUp = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const amount = parseInt(new FormData(form).get("topUpAmount") as string);
const amount =
parseInt(new FormData(form).get("topUpAmount") as string) * 100;
toastOnFail("request top-up", () => requestTopUp(amount));
};
@ -67,8 +69,8 @@ export default function CreditsPage() {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const amount = parseInt(formData.get("topUpAmount") as string);
const threshold = parseInt(formData.get("threshold") as string);
const amount = parseInt(formData.get("topUpAmount") as string) * 100;
const threshold = parseInt(formData.get("threshold") as string) * 100;
toastOnFail("update auto top-up config", () =>
updateAutoTopUpConfig(amount, threshold).then(() => {
toast({ title: "Auto top-up config updated! 🎉" });
@ -108,16 +110,16 @@ export default function CreditsPage() {
htmlFor="topUpAmount"
className="mb-1 block text-neutral-700"
>
Amount, minimum 500 credits = 5 USD:
Top-up amount (USD), minimum $5:
</label>
<input
type="number"
id="topUpAmount"
name="topUpAmount"
placeholder="Enter top-up amount"
min="500"
step="100"
defaultValue={500}
min="5"
step="1"
defaultValue={5}
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
@ -143,10 +145,14 @@ export default function CreditsPage() {
type="number"
id="threshold"
name="threshold"
defaultValue={autoTopUpConfig?.threshold || ""}
placeholder="Amount, minimum 500 credits = 5 USD"
min="500"
step="100"
defaultValue={
autoTopUpConfig?.threshold
? autoTopUpConfig.threshold / 100
: ""
}
placeholder="Refill threshold, minimum $5"
min="5"
step="1"
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
@ -163,10 +169,12 @@ export default function CreditsPage() {
type="number"
id="autoTopUpAmount"
name="topUpAmount"
defaultValue={autoTopUpConfig?.amount || ""}
placeholder="Amount, minimum 500 credits = 5 USD"
min="500"
step="100"
defaultValue={
autoTopUpConfig?.amount ? autoTopUpConfig.amount / 100 : ""
}
placeholder="Refill amount, minimum $5"
min="5"
step="1"
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
@ -253,9 +261,9 @@ export default function CreditsPage() {
transaction.amount > 0 ? "text-green-500" : "text-red-500"
}
>
<b>{transaction.amount}</b>
<b>{formatCredits(transaction.amount)}</b>
</TableCell>
<TableCell>{transaction.balance}</TableCell>
<TableCell>{formatCredits(transaction.balance)}</TableCell>
</TableRow>
))}
</TableBody>

View File

@ -54,6 +54,8 @@ import {
ExitIcon,
} from "@radix-ui/react-icons";
import useCredits from "@/hooks/useCredits";
export type ConnectionData = Array<{
edge_id: string;
source: string;
@ -110,6 +112,7 @@ export function CustomNode({
const isInitialSetup = useRef(true);
const flowContext = useContext(FlowContext);
const api = useBackendAPI();
const { formatCredits } = useCredits();
let nodeFlowId = "";
if (data.uiType === BlockUIType.AGENT) {
@ -712,9 +715,10 @@ export function CustomNode({
<span className="ml-auto flex items-center">
<IconCoin />{" "}
<span className="mx-1 font-medium">
{blockCost.cost_amount}
</span>{" "}
credits/{blockCost.cost_type}
{formatCredits(blockCost.cost_amount)}
</span>
{" \/ "}
{blockCost.cost_type}
</span>
</div>
)}

View File

@ -1,84 +0,0 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import Image from "next/image";
import getServerUser from "@/lib/supabase/getServerUser";
import ProfileDropdown from "./ProfileDropdown";
import { IconCircleUser, IconMenu } from "@/components/ui/icons";
import CreditButton from "@/components/nav/CreditButton";
import { NavBarButtons } from "./nav/NavBarButtons";
export async function NavBar() {
const isAvailable = Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
const { user } = await getServerUser();
return user ? (
<header className="sticky top-0 z-50 mx-4 flex h-16 select-none items-center gap-4 border border-gray-300 bg-background p-3 md:rounded-b-2xl md:px-6 md:shadow">
<div className="flex flex-1 items-center gap-4">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<IconMenu />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="grid gap-6 text-lg font-medium">
<NavBarButtons className="flex flex-row items-center gap-2" />
</nav>
</SheetContent>
</Sheet>
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-8">
<div className="flex h-10 w-20 flex-1 flex-row items-center justify-center gap-2">
<a href="https://agpt.co/">
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={40}
priority
/>
</a>
</div>
<NavBarButtons className="flex flex-row items-center gap-1 border border-white font-semibold hover:border-gray-900" />
</nav>
</div>
<div className="flex flex-1 items-center justify-end gap-4">
{isAvailable && user && <CreditButton />}
{isAvailable && !user && (
<Link
href="/login"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
Log In
<IconCircleUser />
</Link>
)}
{isAvailable && user && <ProfileDropdown />}
</div>
</header>
) : (
<nav className="flex w-full items-center p-2 pt-8">
<div className="flex h-10 w-20 flex-1 flex-row items-center justify-center gap-2">
<a href="https://agpt.co/">
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={40}
priority
/>
</a>
</div>
</nav>
);
}

View File

@ -8,28 +8,21 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import useCredits from "@/hooks/useCredits";
interface CreditsCardProps {
credits: number;
}
const CreditsCard = ({ credits }: CreditsCardProps) => {
const [currentCredits, setCurrentCredits] = useState(credits);
const CreditsCard = () => {
const { credits, formatCredits, fetchCredits } = useCredits();
const api = useBackendAPI();
const onRefresh = async () => {
const { credits } = await api.getUserCredit("credits-card");
setCurrentCredits(credits);
fetchCredits();
};
return (
<div className="inline-flex h-[48px] items-center gap-2.5 rounded-2xl bg-neutral-200 p-4 dark:bg-neutral-800">
<div className="flex items-center gap-0.5">
<span className="p-ui-semibold text-base leading-7 text-neutral-900 dark:text-neutral-50">
{currentCredits.toLocaleString()}
</span>
<span className="p-ui pl-1 text-base leading-7 text-neutral-900 dark:text-neutral-50">
credits
Balance: {formatCredits(credits)}
</span>
</div>
<Tooltip key="RefreshCredits" delayDuration={500}>

View File

@ -33,26 +33,17 @@ interface NavbarProps {
async function getProfileData() {
const api = new BackendAPI();
const [profile, credits] = await Promise.all([
api.getStoreProfile("navbar"),
api.getUserCredit("navbar"),
]);
const profile = await Promise.resolve(api.getStoreProfile("navbar"));
return {
profile,
credits,
};
return profile;
}
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
let credits: { credits: number } = { credits: 0 };
if (isLoggedIn) {
const { profile: t_profile, credits: t_credits } = await getProfileData();
profile = t_profile;
credits = t_credits;
profile = await getProfileData();
}
return (
@ -70,7 +61,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
<div className="flex items-center gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <CreditsCard credits={credits.credits} />}
{profile && <CreditsCard />}
<ProfilePopoutMenu
menuItemGroups={menuItemGroups}
userName={profile?.username}

View File

@ -1,24 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { IconRefresh } from "@/components/ui/icons";
import useCredits from "@/hooks/useCredits";
export default function CreditButton() {
const { credits, fetchCredits } = useCredits();
return (
credits !== null && (
<Button
onClick={fetchCredits}
variant="outline"
className="flex items-center space-x-2 rounded-xl bg-gray-200"
>
<span className="mr-2 flex items-center text-foreground">
{credits} <span className="ml-2 text-muted-foreground"> credits</span>
</span>
<IconRefresh />
</Button>
)
);
}

View File

@ -1,35 +0,0 @@
import { ButtonHTMLAttributes } from "react";
import React from "react";
interface MarketPopupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
marketplaceUrl?: string;
}
export default function MarketPopup({
className = "",
marketplaceUrl = (() => {
if (process.env.NEXT_PUBLIC_APP_ENV === "prod") {
return "https://production-marketplace-url.com";
} else if (process.env.NEXT_PUBLIC_APP_ENV === "dev") {
return "https://dev-builder.agpt.co/marketplace";
} else {
return "http://localhost:3000/marketplace";
}
})(),
children,
...props
}: MarketPopupProps) {
const openMarketplacePopup = () => {
window.open(
marketplaceUrl,
"popupWindow",
"width=600,height=400,toolbar=no,menubar=no,scrollbars=no",
);
};
return (
<button onClick={openMarketplacePopup} className={className} {...props}>
{children}
</button>
);
}

View File

@ -1,83 +0,0 @@
"use client";
import React from "react";
import Link from "next/link";
import { BsBoxes } from "react-icons/bs";
import { LuLaptop, LuShoppingCart } from "react-icons/lu";
import { BehaveAs, cn } from "@/lib/utils";
import { usePathname } from "next/navigation";
import { getBehaveAs } from "@/lib/utils";
import { IconMarketplace } from "@/components/ui/icons";
import MarketPopup from "./MarketPopup";
export function NavBarButtons({ className }: { className?: string }) {
const pathname = usePathname();
const buttons = [
{
href: "/",
text: "Monitor",
icon: <LuLaptop />,
},
{
href: "/build",
text: "Build",
icon: <BsBoxes />,
},
{
href: "/marketplace",
text: "Marketplace",
icon: <IconMarketplace />,
},
];
const isCloud = getBehaveAs() === BehaveAs.CLOUD;
return (
<>
{buttons.map((button) => {
const isActive = button.href === pathname;
return (
<Link
key={button.href}
href={button.href}
data-testid={`${button.text.toLowerCase()}-nav-link`}
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3",
isActive
? "bg-gray-950 text-white"
: "text-muted-foreground hover:text-foreground",
)}
>
{button.icon} {button.text}
</Link>
);
})}
{isCloud ? (
<Link
href="/marketplace"
data-testid="marketplace-nav-link"
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3",
pathname === "/marketplace"
? "bg-gray-950 text-white"
: "text-muted-foreground hover:text-foreground",
)}
>
<LuShoppingCart /> Marketplace
</Link>
) : (
<MarketPopup
data-testid="marketplace-nav-link"
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3 text-muted-foreground hover:text-foreground",
)}
>
<LuShoppingCart /> Marketplace
</MarketPopup>
)}
</>
);
}

View File

@ -17,6 +17,7 @@ export default function useCredits(): {
updateAutoTopUpConfig: (amount: number, threshold: number) => Promise<void>;
transactionHistory: TransactionHistory;
fetchTransactionHistory: () => void;
formatCredits: (credit: number | null) => string;
} {
const [credits, setCredits] = useState<number | null>(null);
const [autoTopUpConfig, setAutoTopUpConfig] = useState<{
@ -89,6 +90,20 @@ export default function useCredits(): {
useEffect(() => {
fetchTransactionHistory();
// Note: We only need to fetch transaction history once.
// Hence, we should avoid `fetchTransactionHistory` to the dependency array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formatCredits = useCallback((credit: number | null) => {
if (credit === null) {
return "-";
}
const value = Math.abs(credit);
const sign = credit < 0 ? "-" : "";
const precision =
2 - (value % 100 === 0 ? 1 : 0) - (value % 10 === 0 ? 1 : 0);
return `${sign}$${(value / 100).toFixed(precision)}`;
}, []);
return {
@ -100,5 +115,6 @@ export default function useCredits(): {
updateAutoTopUpConfig,
transactionHistory,
fetchTransactionHistory,
formatCredits,
};
}