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
parent
533d120e98
commit
243122311c
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue