forked from Significant-Gravitas/AutoGPT
Compare commits
6 Commits
master
...
pwuts/open
Author | SHA1 | Date |
---|---|---|
|
de44d7eb01 | |
|
427258115e | |
|
e53f1eaf80 | |
|
04915f2db0 | |
|
9d79bfadea | |
|
5f50c4863d |
|
@ -15,6 +15,9 @@ REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=password
|
REDIS_PASSWORD=password
|
||||||
|
|
||||||
ENABLE_CREDIT=false
|
ENABLE_CREDIT=false
|
||||||
|
STRIPE_API_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
# What environment things should be logged under: local dev or prod
|
# What environment things should be logged under: local dev or prod
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
# What environment to behave as: "local" or "cloud"
|
# What environment to behave as: "local" or "cloud"
|
||||||
|
@ -36,7 +39,7 @@ SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||||
## to use the platform's webhook-related functionality.
|
## to use the platform's webhook-related functionality.
|
||||||
## If you are developing locally, you can use something like ngrok to get a publc URL
|
## If you are developing locally, you can use something like ngrok to get a publc URL
|
||||||
## and tunnel it to your locally running backend.
|
## and tunnel it to your locally running backend.
|
||||||
PLATFORM_BASE_URL=https://your-public-url-here
|
PLATFORM_BASE_URL=http://localhost:3000
|
||||||
|
|
||||||
## == INTEGRATION CREDENTIALS == ##
|
## == INTEGRATION CREDENTIALS == ##
|
||||||
# Each set of server side credentials is required for the corresponding 3rd party
|
# Each set of server side credentials is required for the corresponding 3rd party
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,32 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import stripe
|
||||||
from prisma import Json
|
from prisma import Json
|
||||||
from prisma.enums import CreditTransactionType
|
from prisma.enums import CreditTransactionType
|
||||||
from prisma.errors import UniqueViolationError
|
from prisma.errors import UniqueViolationError
|
||||||
from prisma.models import CreditTransaction
|
from prisma.models import CreditTransaction, User
|
||||||
|
from prisma.types import CreditTransactionCreateInput, CreditTransactionWhereInput
|
||||||
|
|
||||||
|
from backend.data import db
|
||||||
from backend.data.block import Block, BlockInput, get_block
|
from backend.data.block import Block, BlockInput, get_block
|
||||||
from backend.data.block_cost_config import BLOCK_COSTS
|
from backend.data.block_cost_config import BLOCK_COSTS
|
||||||
from backend.data.cost import BlockCost, BlockCostType
|
from backend.data.cost import BlockCost, BlockCostType
|
||||||
from backend.util.settings import Config
|
from backend.data.user import get_user_by_id
|
||||||
|
from backend.util.settings import Settings
|
||||||
|
|
||||||
config = Config()
|
settings = Settings()
|
||||||
|
stripe.api_key = settings.secrets.stripe_api_key
|
||||||
|
|
||||||
|
|
||||||
class UserCreditBase(ABC):
|
class UserCreditBase(ABC):
|
||||||
def __init__(self, num_user_credits_refill: int):
|
|
||||||
self.num_user_credits_refill = num_user_credits_refill
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_or_refill_credit(self, user_id: str) -> int:
|
async def get_credits(self, user_id: str) -> int:
|
||||||
"""
|
"""
|
||||||
Get the current credit for the user and refill if no transaction has been made in the current cycle.
|
Get the current credits for the user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: The current credit for the user.
|
int: The current credits for the user.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -32,7 +34,6 @@ class UserCreditBase(ABC):
|
||||||
async def spend_credits(
|
async def spend_credits(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
user_credit: int,
|
|
||||||
block_id: str,
|
block_id: str,
|
||||||
input_data: BlockInput,
|
input_data: BlockInput,
|
||||||
data_size: float,
|
data_size: float,
|
||||||
|
@ -43,7 +44,6 @@ class UserCreditBase(ABC):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id (str): The user ID.
|
user_id (str): The user ID.
|
||||||
user_credit (int): The current credit for the user.
|
|
||||||
block_id (str): The block ID.
|
block_id (str): The block ID.
|
||||||
input_data (BlockInput): The input data for the block.
|
input_data (BlockInput): The input data for the block.
|
||||||
data_size (float): The size of the data being processed.
|
data_size (float): The size of the data being processed.
|
||||||
|
@ -57,7 +57,7 @@ class UserCreditBase(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def top_up_credits(self, user_id: str, amount: int):
|
async def top_up_credits(self, user_id: str, amount: int):
|
||||||
"""
|
"""
|
||||||
Top up the credits for the user.
|
Top up the credits for the user immediately.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id (str): The user ID.
|
user_id (str): The user ID.
|
||||||
|
@ -65,51 +65,139 @@ class UserCreditBase(ABC):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def top_up_intent(self, user_id: str, amount: int) -> str:
|
||||||
|
"""
|
||||||
|
Create a payment intent to top up the credits for the user.
|
||||||
|
|
||||||
class UserCredit(UserCreditBase):
|
Args:
|
||||||
async def get_or_refill_credit(self, user_id: str) -> int:
|
user_id (str): The user ID.
|
||||||
cur_time = self.time_now()
|
amount (int): The amount of credits to top up.
|
||||||
cur_month = cur_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
nxt_month = (
|
Returns:
|
||||||
cur_month.replace(month=cur_month.month + 1)
|
str: The redirect url to the payment page.
|
||||||
if cur_month.month < 12
|
"""
|
||||||
else cur_month.replace(year=cur_month.year + 1, month=1)
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def fulfill_checkout(
|
||||||
|
self, *, session_id: str | None = None, user_id: str | None = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Fulfill the Stripe checkout session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_id (str | None): The checkout session ID. Will try to fulfill most recent if None.
|
||||||
|
user_id (str | None): The user ID must be provided if session_id is None.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def time_now() -> datetime:
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# ====== Transaction Helper Methods ====== #
|
||||||
|
# Any modifications to the transaction table should only be done through these methods #
|
||||||
|
|
||||||
|
async def _get_credits(self, user_id: str) -> tuple[int, datetime]:
|
||||||
|
"""
|
||||||
|
Returns the current balance of the user & the latest balance snapshot time.
|
||||||
|
"""
|
||||||
|
top_time = self.time_now()
|
||||||
|
snapshot = await CreditTransaction.prisma().find_first(
|
||||||
|
where={
|
||||||
|
"userId": user_id,
|
||||||
|
"createdAt": {"lte": top_time},
|
||||||
|
"isActive": True,
|
||||||
|
"runningBalance": {"not": None}, # type: ignore
|
||||||
|
},
|
||||||
|
order={"createdAt": "desc"},
|
||||||
)
|
)
|
||||||
|
if snapshot:
|
||||||
|
return snapshot.runningBalance or 0, snapshot.createdAt
|
||||||
|
|
||||||
user_credit = await CreditTransaction.prisma().group_by(
|
# No snapshot: Manually calculate balance using current month's transactions.
|
||||||
|
low_time = top_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
transactions = await CreditTransaction.prisma().group_by(
|
||||||
by=["userId"],
|
by=["userId"],
|
||||||
sum={"amount": True},
|
sum={"amount": True},
|
||||||
where={
|
where={
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"createdAt": {"gte": cur_month, "lt": nxt_month},
|
"createdAt": {"gte": low_time, "lte": top_time},
|
||||||
"isActive": True,
|
"isActive": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
transaction_balance = (
|
||||||
|
transactions[0].get("_sum", {}).get("amount", 0) if transactions else 0
|
||||||
|
)
|
||||||
|
return transaction_balance, datetime.min
|
||||||
|
|
||||||
if user_credit:
|
async def _enable_transaction(
|
||||||
credit_sum = user_credit[0].get("_sum") or {}
|
self, transaction_key: str, user_id: str, metadata: Json
|
||||||
return credit_sum.get("amount", 0)
|
):
|
||||||
|
|
||||||
key = f"MONTHLY-CREDIT-TOP-UP-{cur_month}"
|
transaction = await CreditTransaction.prisma().find_first_or_raise(
|
||||||
|
where={"transactionKey": transaction_key, "userId": user_id}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
if transaction.isActive:
|
||||||
await CreditTransaction.prisma().create(
|
return
|
||||||
|
|
||||||
|
async with db.locked_transaction(f"usr_trx_{user_id}"):
|
||||||
|
user_balance, _ = await self._get_credits(user_id)
|
||||||
|
|
||||||
|
await CreditTransaction.prisma().update(
|
||||||
|
where={
|
||||||
|
"creditTransactionIdentifier": {
|
||||||
|
"transactionKey": transaction_key,
|
||||||
|
"userId": user_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
data={
|
data={
|
||||||
"amount": self.num_user_credits_refill,
|
"isActive": True,
|
||||||
"type": CreditTransactionType.TOP_UP,
|
"runningBalance": user_balance + transaction.amount,
|
||||||
"userId": user_id,
|
|
||||||
"transactionKey": key,
|
|
||||||
"createdAt": self.time_now(),
|
"createdAt": self.time_now(),
|
||||||
}
|
"metadata": metadata,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
except UniqueViolationError:
|
|
||||||
pass # Already refilled this month
|
|
||||||
|
|
||||||
return self.num_user_credits_refill
|
async def _add_transaction(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
amount: int,
|
||||||
|
transaction_type: CreditTransactionType,
|
||||||
|
is_active: bool = True,
|
||||||
|
transaction_key: str | None = None,
|
||||||
|
block_id: str | None = None,
|
||||||
|
metadata: Json = Json({}),
|
||||||
|
):
|
||||||
|
async with db.locked_transaction(f"usr_trx_{user_id}"):
|
||||||
|
# Get latest balance snapshot
|
||||||
|
user_balance, _ = await self._get_credits(user_id)
|
||||||
|
if amount < 0 and user_balance < abs(amount):
|
||||||
|
raise ValueError(
|
||||||
|
f"Insufficient balance for user {user_id}, balance: {user_balance}, amount: {amount}"
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
# Create the transaction
|
||||||
def time_now():
|
transaction_data: CreditTransactionCreateInput = {
|
||||||
return datetime.now(timezone.utc)
|
"userId": user_id,
|
||||||
|
"amount": amount,
|
||||||
|
"runningBalance": user_balance + amount,
|
||||||
|
"type": transaction_type,
|
||||||
|
"blockId": block_id,
|
||||||
|
"metadata": metadata,
|
||||||
|
"isActive": is_active,
|
||||||
|
"createdAt": self.time_now(),
|
||||||
|
}
|
||||||
|
if transaction_key:
|
||||||
|
transaction_data["transactionKey"] = transaction_key
|
||||||
|
await CreditTransaction.prisma().create(data=transaction_data)
|
||||||
|
|
||||||
|
return user_balance + amount
|
||||||
|
|
||||||
|
|
||||||
|
class UserCredit(UserCreditBase):
|
||||||
|
|
||||||
def _block_usage_cost(
|
def _block_usage_cost(
|
||||||
self,
|
self,
|
||||||
|
@ -148,8 +236,8 @@ class UserCredit(UserCreditBase):
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Filter rules:
|
Filter rules:
|
||||||
- If costFilter is an object, then check if costFilter is the subset of inputValues
|
- If cost_filter is an object, then check if cost_filter is the subset of input_data
|
||||||
- Otherwise, check if costFilter is equal to inputValues.
|
- Otherwise, check if cost_filter is equal to input_data.
|
||||||
- Undefined, null, and empty string are considered as equal.
|
- Undefined, null, and empty string are considered as equal.
|
||||||
"""
|
"""
|
||||||
if not isinstance(cost_filter, dict) or not isinstance(input_data, dict):
|
if not isinstance(cost_filter, dict) or not isinstance(input_data, dict):
|
||||||
|
@ -164,12 +252,10 @@ class UserCredit(UserCreditBase):
|
||||||
async def spend_credits(
|
async def spend_credits(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
user_credit: int,
|
|
||||||
block_id: str,
|
block_id: str,
|
||||||
input_data: BlockInput,
|
input_data: BlockInput,
|
||||||
data_size: float,
|
data_size: float,
|
||||||
run_time: float,
|
run_time: float,
|
||||||
validate_balance: bool = True,
|
|
||||||
) -> int:
|
) -> int:
|
||||||
block = get_block(block_id)
|
block = get_block(block_id)
|
||||||
if not block:
|
if not block:
|
||||||
|
@ -178,42 +264,169 @@ class UserCredit(UserCreditBase):
|
||||||
cost, matching_filter = self._block_usage_cost(
|
cost, matching_filter = self._block_usage_cost(
|
||||||
block=block, input_data=input_data, data_size=data_size, run_time=run_time
|
block=block, input_data=input_data, data_size=data_size, run_time=run_time
|
||||||
)
|
)
|
||||||
if cost <= 0:
|
if cost == 0:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if validate_balance and user_credit < cost:
|
await self._add_transaction(
|
||||||
raise ValueError(f"Insufficient credit: {user_credit} < {cost}")
|
user_id=user_id,
|
||||||
|
amount=-cost,
|
||||||
await CreditTransaction.prisma().create(
|
transaction_type=CreditTransactionType.USAGE,
|
||||||
data={
|
block_id=block.id,
|
||||||
"userId": user_id,
|
metadata=Json(
|
||||||
"amount": -cost,
|
{
|
||||||
"type": CreditTransactionType.USAGE,
|
"block": block.name,
|
||||||
"blockId": block.id,
|
"input": matching_filter,
|
||||||
"metadata": Json(
|
}
|
||||||
{
|
),
|
||||||
"block": block.name,
|
|
||||||
"input": matching_filter,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"createdAt": self.time_now(),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
async def top_up_credits(self, user_id: str, amount: int):
|
async def top_up_credits(self, user_id: str, amount: int):
|
||||||
await CreditTransaction.prisma().create(
|
if amount < 0:
|
||||||
data={
|
raise ValueError(f"Top up amount must not be negative: {amount}")
|
||||||
"userId": user_id,
|
|
||||||
"amount": amount,
|
await self._add_transaction(
|
||||||
"type": CreditTransactionType.TOP_UP,
|
user_id=user_id,
|
||||||
"createdAt": self.time_now(),
|
amount=amount,
|
||||||
}
|
transaction_type=CreditTransactionType.TOP_UP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _get_stripe_customer_id(user_id: str) -> str:
|
||||||
|
user = await get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
raise ValueError(f"User not found: {user_id}")
|
||||||
|
|
||||||
|
if user.stripeCustomerId:
|
||||||
|
return user.stripeCustomerId
|
||||||
|
|
||||||
|
customer = stripe.Customer.create(name=user.name or "", email=user.email)
|
||||||
|
await User.prisma().update(
|
||||||
|
where={"id": user_id}, data={"stripeCustomerId": customer.id}
|
||||||
|
)
|
||||||
|
return customer.id
|
||||||
|
|
||||||
|
async def top_up_intent(self, user_id: str, amount: int) -> str:
|
||||||
|
# Create checkout session
|
||||||
|
# https://docs.stripe.com/checkout/quickstart?client=react
|
||||||
|
# unit_amount param is always in the smallest currency unit (so cents for usd)
|
||||||
|
# which is equal to amount of credits
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
customer=await self._get_stripe_customer_id(user_id),
|
||||||
|
line_items=[
|
||||||
|
{
|
||||||
|
"price_data": {
|
||||||
|
"currency": "usd",
|
||||||
|
"product_data": {
|
||||||
|
"name": "AutoGPT Platform Credits",
|
||||||
|
},
|
||||||
|
"unit_amount": amount,
|
||||||
|
},
|
||||||
|
"quantity": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mode="payment",
|
||||||
|
success_url=settings.config.platform_base_url
|
||||||
|
+ "/store/credits?topup=success",
|
||||||
|
cancel_url=settings.config.platform_base_url
|
||||||
|
+ "/store/credits?topup=cancel",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create pending transaction
|
||||||
|
await self._add_transaction(
|
||||||
|
user_id=user_id,
|
||||||
|
amount=amount,
|
||||||
|
transaction_type=CreditTransactionType.TOP_UP,
|
||||||
|
transaction_key=checkout_session.id,
|
||||||
|
is_active=False,
|
||||||
|
metadata=Json({"checkout_session": checkout_session}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return checkout_session.url or ""
|
||||||
|
|
||||||
|
# https://docs.stripe.com/checkout/fulfillment
|
||||||
|
async def fulfill_checkout(
|
||||||
|
self, *, session_id: str | None = None, user_id: str | None = None
|
||||||
|
):
|
||||||
|
if (not session_id and not user_id) or (session_id and user_id):
|
||||||
|
raise ValueError("Either session_id or user_id must be provided")
|
||||||
|
|
||||||
|
# Retrieve CreditTransaction
|
||||||
|
find_filter: CreditTransactionWhereInput = {
|
||||||
|
"type": CreditTransactionType.TOP_UP,
|
||||||
|
"isActive": False,
|
||||||
|
}
|
||||||
|
if session_id:
|
||||||
|
find_filter["transactionKey"] = session_id
|
||||||
|
if user_id:
|
||||||
|
find_filter["userId"] = user_id
|
||||||
|
|
||||||
|
# Find the most recent inactive top-up transaction
|
||||||
|
credit_transaction = await CreditTransaction.prisma().find_first_or_raise(
|
||||||
|
where=find_filter,
|
||||||
|
order={"createdAt": "desc"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# This can be called multiple times for one id, so ignore if already fulfilled
|
||||||
|
if not credit_transaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Retrieve the Checkout Session from the API
|
||||||
|
checkout_session = stripe.checkout.Session.retrieve(
|
||||||
|
credit_transaction.transactionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the Checkout Session's payment_status property
|
||||||
|
# to determine if fulfillment should be performed
|
||||||
|
if checkout_session.payment_status in ["paid", "no_payment_required"]:
|
||||||
|
await self._enable_transaction(
|
||||||
|
transaction_key=credit_transaction.transactionKey,
|
||||||
|
user_id=credit_transaction.userId,
|
||||||
|
metadata=Json({"checkout_session": checkout_session}),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_credits(self, user_id: str) -> int:
|
||||||
|
balance, _ = await self._get_credits(user_id)
|
||||||
|
return balance
|
||||||
|
|
||||||
|
|
||||||
|
class BetaUserCredit(UserCredit):
|
||||||
|
"""
|
||||||
|
This is a temporary class to handle the test user utilizing monthly credit refill.
|
||||||
|
TODO: Remove this class & its feature toggle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, num_user_credits_refill: int):
|
||||||
|
self.num_user_credits_refill = num_user_credits_refill
|
||||||
|
|
||||||
|
async def get_credits(self, user_id: str) -> int:
|
||||||
|
cur_time = self.time_now().date()
|
||||||
|
balance, snapshot_time = await self._get_credits(user_id)
|
||||||
|
if (snapshot_time.year, snapshot_time.month) == (cur_time.year, cur_time.month):
|
||||||
|
return balance
|
||||||
|
|
||||||
|
try:
|
||||||
|
await CreditTransaction.prisma().create(
|
||||||
|
data={
|
||||||
|
"transactionKey": f"MONTHLY-CREDIT-TOP-UP-{cur_time}",
|
||||||
|
"userId": user_id,
|
||||||
|
"amount": self.num_user_credits_refill,
|
||||||
|
"runningBalance": self.num_user_credits_refill,
|
||||||
|
"type": CreditTransactionType.TOP_UP,
|
||||||
|
"metadata": Json({}),
|
||||||
|
"isActive": True,
|
||||||
|
"createdAt": self.time_now(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except UniqueViolationError:
|
||||||
|
pass # Already refilled this month
|
||||||
|
|
||||||
|
return self.num_user_credits_refill
|
||||||
|
|
||||||
|
|
||||||
class DisabledUserCredit(UserCreditBase):
|
class DisabledUserCredit(UserCreditBase):
|
||||||
async def get_or_refill_credit(self, *args, **kwargs) -> int:
|
async def get_credits(self, *args, **kwargs) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def spend_credits(self, *args, **kwargs) -> int:
|
async def spend_credits(self, *args, **kwargs) -> int:
|
||||||
|
@ -222,12 +435,21 @@ class DisabledUserCredit(UserCreditBase):
|
||||||
async def top_up_credits(self, *args, **kwargs):
|
async def top_up_credits(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def top_up_intent(self, *args, **kwargs) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def fulfill_checkout(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_user_credit_model() -> UserCreditBase:
|
def get_user_credit_model() -> UserCreditBase:
|
||||||
if config.enable_credit.lower() == "true":
|
if not settings.config.enable_credit:
|
||||||
return UserCredit(config.num_user_credits_refill)
|
return DisabledUserCredit()
|
||||||
else:
|
|
||||||
return DisabledUserCredit(0)
|
if settings.config.enable_beta_monthly_credit:
|
||||||
|
return BetaUserCredit(settings.config.num_user_credits_refill)
|
||||||
|
|
||||||
|
return UserCredit()
|
||||||
|
|
||||||
|
|
||||||
def get_block_costs() -> dict[str, list[BlockCost]]:
|
def get_block_costs() -> dict[str, list[BlockCost]]:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import zlib
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
@ -54,6 +55,14 @@ async def transaction():
|
||||||
yield tx
|
yield tx
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def locked_transaction(key: str):
|
||||||
|
lock_key = zlib.crc32(key.encode("utf-8"))
|
||||||
|
async with transaction() as tx:
|
||||||
|
await tx.execute_raw(f"SELECT pg_advisory_xact_lock({lock_key})")
|
||||||
|
yield tx
|
||||||
|
|
||||||
|
|
||||||
class BaseDbModel(BaseModel):
|
class BaseDbModel(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||||
|
|
||||||
|
|
|
@ -78,12 +78,8 @@ class DatabaseManager(AppService):
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
user_credit_model = get_user_credit_model()
|
user_credit_model = get_user_credit_model()
|
||||||
get_or_refill_credit = cast(
|
|
||||||
Callable[[Any, str], int],
|
|
||||||
exposed_run_and_wait(user_credit_model.get_or_refill_credit),
|
|
||||||
)
|
|
||||||
spend_credits = cast(
|
spend_credits = cast(
|
||||||
Callable[[Any, str, int, str, dict[str, str], float, float], int],
|
Callable[[Any, str, str, dict[str, str], float, float], int],
|
||||||
exposed_run_and_wait(user_credit_model.spend_credits),
|
exposed_run_and_wait(user_credit_model.spend_credits),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -183,9 +183,6 @@ def execute_node(
|
||||||
|
|
||||||
output_size = 0
|
output_size = 0
|
||||||
end_status = ExecutionStatus.COMPLETED
|
end_status = ExecutionStatus.COMPLETED
|
||||||
credit = db_client.get_or_refill_credit(user_id)
|
|
||||||
if credit < 0:
|
|
||||||
raise ValueError(f"Insufficient credit: {credit}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for output_name, output_data in node_block.execute(
|
for output_name, output_data in node_block.execute(
|
||||||
|
@ -241,7 +238,7 @@ def execute_node(
|
||||||
if res.end_time and res.start_time
|
if res.end_time and res.start_time
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
db_client.spend_credits(user_id, credit, node_block.id, input_data, s, t)
|
db_client.spend_credits(user_id, node_block.id, input_data, s, t)
|
||||||
|
|
||||||
# Update execution stats
|
# Update execution stats
|
||||||
if execution_stats is not None:
|
if execution_stats is not None:
|
||||||
|
|
|
@ -23,6 +23,15 @@ from backend.util.settings import Settings
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
# This is an overrride since ollama doesn't actually require an API key, but the creddential system enforces one be attached
|
||||||
|
ollama_credentials = APIKeyCredentials(
|
||||||
|
id="744fdc56-071a-4761-b5a5-0af0ce10a2b5",
|
||||||
|
provider="ollama",
|
||||||
|
api_key=SecretStr("FAKE_API_KEY"),
|
||||||
|
title="Use Credits for Ollama",
|
||||||
|
expires_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
revid_credentials = APIKeyCredentials(
|
revid_credentials = APIKeyCredentials(
|
||||||
id="fdb7f412-f519-48d1-9b5f-d2f73d0e01fe",
|
id="fdb7f412-f519-48d1-9b5f-d2f73d0e01fe",
|
||||||
provider="revid",
|
provider="revid",
|
||||||
|
@ -124,6 +133,7 @@ nvidia_credentials = APIKeyCredentials(
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CREDENTIALS = [
|
DEFAULT_CREDENTIALS = [
|
||||||
|
ollama_credentials,
|
||||||
revid_credentials,
|
revid_credentials,
|
||||||
ideogram_credentials,
|
ideogram_credentials,
|
||||||
replicate_credentials,
|
replicate_credentials,
|
||||||
|
@ -169,6 +179,10 @@ class IntegrationCredentialsStore:
|
||||||
def get_all_creds(self, user_id: str) -> list[Credentials]:
|
def get_all_creds(self, user_id: str) -> list[Credentials]:
|
||||||
users_credentials = self._get_user_integrations(user_id).credentials
|
users_credentials = self._get_user_integrations(user_id).credentials
|
||||||
all_credentials = users_credentials
|
all_credentials = users_credentials
|
||||||
|
# These will always be added
|
||||||
|
all_credentials.append(ollama_credentials)
|
||||||
|
|
||||||
|
# These will only be added if the API key is set
|
||||||
if settings.secrets.revid_api_key:
|
if settings.secrets.revid_api_key:
|
||||||
all_credentials.append(revid_credentials)
|
all_credentials.append(revid_credentials)
|
||||||
if settings.secrets.ideogram_api_key:
|
if settings.secrets.ideogram_api_key:
|
||||||
|
|
|
@ -56,3 +56,8 @@ class SetGraphActiveVersion(pydantic.BaseModel):
|
||||||
|
|
||||||
class UpdatePermissionsRequest(pydantic.BaseModel):
|
class UpdatePermissionsRequest(pydantic.BaseModel):
|
||||||
permissions: List[APIKeyPermission]
|
permissions: List[APIKeyPermission]
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTopUp(pydantic.BaseModel):
|
||||||
|
amount: int
|
||||||
|
"""Amount of credits to top up."""
|
||||||
|
|
|
@ -4,10 +4,11 @@ from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, Annotated, Any, Sequence
|
from typing import TYPE_CHECKING, Annotated, Any, Sequence
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
|
import stripe
|
||||||
from autogpt_libs.auth.middleware import auth_middleware
|
from autogpt_libs.auth.middleware import auth_middleware
|
||||||
from autogpt_libs.feature_flag.client import feature_flag
|
from autogpt_libs.feature_flag.client import feature_flag
|
||||||
from autogpt_libs.utils.cache import thread_cached
|
from autogpt_libs.utils.cache import thread_cached
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
from typing_extensions import Optional, TypedDict
|
from typing_extensions import Optional, TypedDict
|
||||||
|
|
||||||
import backend.data.block
|
import backend.data.block
|
||||||
|
@ -40,6 +41,7 @@ from backend.server.model import (
|
||||||
CreateAPIKeyRequest,
|
CreateAPIKeyRequest,
|
||||||
CreateAPIKeyResponse,
|
CreateAPIKeyResponse,
|
||||||
CreateGraph,
|
CreateGraph,
|
||||||
|
RequestTopUp,
|
||||||
SetGraphActiveVersion,
|
SetGraphActiveVersion,
|
||||||
UpdatePermissionsRequest,
|
UpdatePermissionsRequest,
|
||||||
)
|
)
|
||||||
|
@ -134,7 +136,54 @@ async def get_user_credits(
|
||||||
user_id: Annotated[str, Depends(get_user_id)],
|
user_id: Annotated[str, Depends(get_user_id)],
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
# Credits can go negative, so ensure it's at least 0 for user to see.
|
# Credits can go negative, so ensure it's at least 0 for user to see.
|
||||||
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}
|
return {"credits": max(await _user_credit_model.get_credits(user_id), 0)}
|
||||||
|
|
||||||
|
|
||||||
|
@v1_router.post(
|
||||||
|
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)]
|
||||||
|
)
|
||||||
|
async def request_top_up(
|
||||||
|
request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)]
|
||||||
|
):
|
||||||
|
checkout_url = await _user_credit_model.top_up_intent(user_id, request.amount)
|
||||||
|
return {"checkout_url": checkout_url}
|
||||||
|
|
||||||
|
|
||||||
|
@v1_router.patch(
|
||||||
|
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)]
|
||||||
|
)
|
||||||
|
async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
|
||||||
|
await _user_credit_model.fulfill_checkout(user_id=user_id)
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@v1_router.post(path="/credits/stripe_webhook", tags=["credits"])
|
||||||
|
async def stripe_webhook(request: Request):
|
||||||
|
# Get the raw request body
|
||||||
|
payload = await request.body()
|
||||||
|
# Get the signature header
|
||||||
|
sig_header = request.headers.get("stripe-signature")
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = stripe.Webhook.construct_event(
|
||||||
|
payload, sig_header, settings.secrets.stripe_webhook_secret
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# Invalid payload
|
||||||
|
raise HTTPException(status_code=400)
|
||||||
|
except stripe.SignatureVerificationError:
|
||||||
|
# Invalid signature
|
||||||
|
raise HTTPException(status_code=400)
|
||||||
|
|
||||||
|
if (
|
||||||
|
event["type"] == "checkout.session.completed"
|
||||||
|
or event["type"] == "checkout.session.async_payment_succeeded"
|
||||||
|
):
|
||||||
|
await _user_credit_model.fulfill_checkout(
|
||||||
|
session_id=event["data"]["object"]["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
|
|
|
@ -81,10 +81,14 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||||
default=True,
|
default=True,
|
||||||
description="If authentication is enabled or not",
|
description="If authentication is enabled or not",
|
||||||
)
|
)
|
||||||
enable_credit: str = Field(
|
enable_credit: bool = Field(
|
||||||
default="false",
|
default=False,
|
||||||
description="If user credit system is enabled or not",
|
description="If user credit system is enabled or not",
|
||||||
)
|
)
|
||||||
|
enable_beta_monthly_credit: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="If beta monthly credits accounting is enabled or not",
|
||||||
|
)
|
||||||
num_user_credits_refill: int = Field(
|
num_user_credits_refill: int = Field(
|
||||||
default=1500,
|
default=1500,
|
||||||
description="Number of credits to refill for each user",
|
description="Number of credits to refill for each user",
|
||||||
|
@ -309,6 +313,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||||
e2b_api_key: str = Field(default="", description="E2B API key")
|
e2b_api_key: str = Field(default="", description="E2B API key")
|
||||||
nvidia_api_key: str = Field(default="", description="Nvidia API key")
|
nvidia_api_key: str = Field(default="", description="Nvidia API key")
|
||||||
|
|
||||||
|
stripe_api_key: str = Field(default="", description="Stripe API Key")
|
||||||
|
stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret")
|
||||||
|
|
||||||
# Add more secret fields as needed
|
# Add more secret fields as needed
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CreditTransaction" ADD COLUMN "runningBalance" INTEGER;
|
|
@ -3688,6 +3688,22 @@ docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
|
||||||
release = ["twine"]
|
release = ["twine"]
|
||||||
test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
|
test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stripe"
|
||||||
|
version = "11.4.1"
|
||||||
|
description = "Python bindings for the Stripe API"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "stripe-11.4.1-py2.py3-none-any.whl", hash = "sha256:8aa47a241de0355c383c916c4ef7273ab666f096a44ee7081e357db4a36f0cce"},
|
||||||
|
{file = "stripe-11.4.1.tar.gz", hash = "sha256:7ddd251b622d490fe57d78487855dc9f4d95b1bb113607e81fd377037a133d5a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
requests = {version = ">=2.20", markers = "python_version >= \"3.0\""}
|
||||||
|
typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "supabase"
|
name = "supabase"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
@ -4432,4 +4448,4 @@ type = ["pytest-mypy"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "711669de9e6d5b81f19286bd41d52f57bc0177ba8ff5f2b477313a5b2d012ae5"
|
content-hash = "341712d286b6a6fae89055bd21a55d8fa918973e446f6c0f0329a8493022cbae"
|
||||||
|
|
|
@ -39,6 +39,7 @@ python-dotenv = "^1.0.1"
|
||||||
redis = "^5.2.0"
|
redis = "^5.2.0"
|
||||||
sentry-sdk = "2.19.2"
|
sentry-sdk = "2.19.2"
|
||||||
strenum = "^0.4.9"
|
strenum = "^0.4.9"
|
||||||
|
stripe = "^11.3.0"
|
||||||
supabase = "2.11.0"
|
supabase = "2.11.0"
|
||||||
tenacity = "^9.0.0"
|
tenacity = "^9.0.0"
|
||||||
tweepy = "^4.14.0"
|
tweepy = "^4.14.0"
|
||||||
|
|
|
@ -393,6 +393,8 @@ model CreditTransaction {
|
||||||
amount Int
|
amount Int
|
||||||
type CreditTransactionType
|
type CreditTransactionType
|
||||||
|
|
||||||
|
runningBalance Int?
|
||||||
|
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
metadata Json?
|
metadata Json?
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,27 @@ import pytest
|
||||||
from prisma.models import CreditTransaction
|
from prisma.models import CreditTransaction
|
||||||
|
|
||||||
from backend.blocks.llm import AITextGeneratorBlock
|
from backend.blocks.llm import AITextGeneratorBlock
|
||||||
from backend.data.credit import UserCredit
|
from backend.data.credit import BetaUserCredit
|
||||||
from backend.data.user import DEFAULT_USER_ID
|
from backend.data.user import DEFAULT_USER_ID
|
||||||
from backend.integrations.credentials_store import openai_credentials
|
from backend.integrations.credentials_store import openai_credentials
|
||||||
from backend.util.test import SpinTestServer
|
from backend.util.test import SpinTestServer
|
||||||
|
|
||||||
REFILL_VALUE = 1000
|
REFILL_VALUE = 1000
|
||||||
user_credit = UserCredit(REFILL_VALUE)
|
user_credit = BetaUserCredit(REFILL_VALUE)
|
||||||
|
|
||||||
|
|
||||||
|
async def disable_test_user_transactions():
|
||||||
|
await CreditTransaction.prisma().delete_many(where={"userId": DEFAULT_USER_ID})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="session")
|
||||||
async def test_block_credit_usage(server: SpinTestServer):
|
async def test_block_credit_usage(server: SpinTestServer):
|
||||||
current_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
await disable_test_user_transactions()
|
||||||
|
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
|
||||||
|
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
|
|
||||||
spending_amount_1 = await user_credit.spend_credits(
|
spending_amount_1 = await user_credit.spend_credits(
|
||||||
DEFAULT_USER_ID,
|
DEFAULT_USER_ID,
|
||||||
current_credit,
|
|
||||||
AITextGeneratorBlock().id,
|
AITextGeneratorBlock().id,
|
||||||
{
|
{
|
||||||
"model": "gpt-4-turbo",
|
"model": "gpt-4-turbo",
|
||||||
|
@ -31,68 +36,56 @@ async def test_block_credit_usage(server: SpinTestServer):
|
||||||
},
|
},
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
validate_balance=False,
|
|
||||||
)
|
)
|
||||||
assert spending_amount_1 > 0
|
assert spending_amount_1 > 0
|
||||||
|
|
||||||
spending_amount_2 = await user_credit.spend_credits(
|
spending_amount_2 = await user_credit.spend_credits(
|
||||||
DEFAULT_USER_ID,
|
DEFAULT_USER_ID,
|
||||||
current_credit,
|
|
||||||
AITextGeneratorBlock().id,
|
AITextGeneratorBlock().id,
|
||||||
{"model": "gpt-4-turbo", "api_key": "owned_api_key"},
|
{"model": "gpt-4-turbo", "api_key": "owned_api_key"},
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
validate_balance=False,
|
|
||||||
)
|
)
|
||||||
assert spending_amount_2 == 0
|
assert spending_amount_2 == 0
|
||||||
|
|
||||||
new_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
new_credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
assert new_credit == current_credit - spending_amount_1 - spending_amount_2
|
assert new_credit == current_credit - spending_amount_1 - spending_amount_2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="session")
|
||||||
async def test_block_credit_top_up(server: SpinTestServer):
|
async def test_block_credit_top_up(server: SpinTestServer):
|
||||||
current_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
await disable_test_user_transactions()
|
||||||
|
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
|
|
||||||
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
|
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
|
||||||
|
|
||||||
new_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
new_credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
assert new_credit == current_credit + 100
|
assert new_credit == current_credit + 100
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="session")
|
||||||
async def test_block_credit_reset(server: SpinTestServer):
|
async def test_block_credit_reset(server: SpinTestServer):
|
||||||
month1 = datetime(2022, 1, 15)
|
await disable_test_user_transactions()
|
||||||
month2 = datetime(2022, 2, 15)
|
month1 = 1
|
||||||
|
month2 = 2
|
||||||
|
|
||||||
user_credit.time_now = lambda: month2
|
# set the calendar to month 2 but use current time from now
|
||||||
month2credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
user_credit.time_now = lambda: datetime.now().replace(month=month2)
|
||||||
|
month2credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
|
|
||||||
# Month 1 result should only affect month 1
|
# Month 1 result should only affect month 1
|
||||||
user_credit.time_now = lambda: month1
|
user_credit.time_now = lambda: datetime.now().replace(month=month1)
|
||||||
month1credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
month1credit = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
|
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
|
||||||
assert await user_credit.get_or_refill_credit(DEFAULT_USER_ID) == month1credit + 100
|
assert await user_credit.get_credits(DEFAULT_USER_ID) == month1credit + 100
|
||||||
|
|
||||||
# Month 2 balance is unaffected
|
# Month 2 balance is unaffected
|
||||||
user_credit.time_now = lambda: month2
|
user_credit.time_now = lambda: datetime.now().replace(month=month2)
|
||||||
assert await user_credit.get_or_refill_credit(DEFAULT_USER_ID) == month2credit
|
assert await user_credit.get_credits(DEFAULT_USER_ID) == month2credit
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="session")
|
||||||
async def test_credit_refill(server: SpinTestServer):
|
async def test_credit_refill(server: SpinTestServer):
|
||||||
# Clear all transactions within the month
|
await disable_test_user_transactions()
|
||||||
await CreditTransaction.prisma().update_many(
|
balance = await user_credit.get_credits(DEFAULT_USER_ID)
|
||||||
where={
|
|
||||||
"userId": DEFAULT_USER_ID,
|
|
||||||
"createdAt": {
|
|
||||||
"gte": datetime(2022, 2, 1),
|
|
||||||
"lt": datetime(2022, 3, 1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data={"isActive": False},
|
|
||||||
)
|
|
||||||
user_credit.time_now = lambda: datetime(2022, 2, 15)
|
|
||||||
|
|
||||||
balance = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
|
|
||||||
assert balance == REFILL_VALUE
|
assert balance == REFILL_VALUE
|
||||||
|
|
|
@ -5,6 +5,7 @@ NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
|
||||||
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
|
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
|
||||||
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
|
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
|
||||||
NEXT_PUBLIC_APP_ENV=dev
|
NEXT_PUBLIC_APP_ENV=dev
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||||
|
|
||||||
## Locale settings
|
## Locale settings
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
"@sentry/nextjs": "^8",
|
"@sentry/nextjs": "^8",
|
||||||
|
"@stripe/stripe-js": "^5.3.0",
|
||||||
"@supabase/ssr": "^0.5.2",
|
"@supabase/ssr": "^0.5.2",
|
||||||
"@supabase/supabase-js": "^2.47.8",
|
"@supabase/supabase-js": "^2.47.8",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
|
@ -64,7 +65,7 @@
|
||||||
"launchdarkly-react-client-sdk": "^3.6.0",
|
"launchdarkly-react-client-sdk": "^3.6.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "^14.2.13",
|
"next": "^14.2.21",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "^9.5.0",
|
"react-day-picker": "^9.5.0",
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default async function RootLayout({
|
||||||
// enableSystem
|
// enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center">
|
<div className="flex flex-col items-stretch justify-items-stretch min-h-screen">
|
||||||
<Navbar
|
<Navbar
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
|
@ -102,7 +102,7 @@ export default async function RootLayout({
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-grow w-full">{children}</main>
|
||||||
<TallyPopupSimple />
|
<TallyPopupSimple />
|
||||||
</div>
|
</div>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthCard>
|
<AuthCard className="mx-auto">
|
||||||
<AuthHeader>Login to your account</AuthHeader>
|
<AuthHeader>Login to your account</AuthHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||||
|
|
|
@ -73,7 +73,7 @@ const Monitor = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10 p-4"
|
||||||
data-testid="monitor-page"
|
data-testid="monitor-page"
|
||||||
>
|
>
|
||||||
<AgentFlowList
|
<AgentFlowList
|
||||||
|
|
|
@ -98,6 +98,7 @@ export default function PrivatePage() {
|
||||||
// This contains ids for built-in "Use Credits for X" credentials
|
// This contains ids for built-in "Use Credits for X" credentials
|
||||||
const hiddenCredentials = useMemo(
|
const hiddenCredentials = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
"744fdc56-071a-4761-b5a5-0af0ce10a2b5", // Ollama
|
||||||
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
|
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
|
||||||
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
|
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
|
||||||
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
|
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default function SignupPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthCard>
|
<AuthCard className="mx-auto">
|
||||||
<AuthHeader>Create a new account</AuthHeader>
|
<AuthHeader>Create a new account</AuthHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
import { Button } from "@/components/agptui/Button";
|
||||||
|
import useCredits from "@/hooks/useCredits";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function CreditsPage() {
|
||||||
|
const { credits, requestTopUp } = useCredits();
|
||||||
|
const [amount, setAmount] = useState(5);
|
||||||
|
const [patched, setPatched] = useState(false);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const topupStatus = searchParams.get("topup");
|
||||||
|
const api = useBackendAPI();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!patched && topupStatus === "success") {
|
||||||
|
api.fulfillCheckout();
|
||||||
|
setPatched(true);
|
||||||
|
}
|
||||||
|
}, [api, patched, topupStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full min-w-[800px] px-4 sm:px-8">
|
||||||
|
<h1 className="font-circular mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]">
|
||||||
|
Credits
|
||||||
|
</h1>
|
||||||
|
<p className="font-circular mb-6 text-base font-normal leading-tight text-neutral-600 dark:text-neutral-400">
|
||||||
|
Current credits: <b>{credits}</b>
|
||||||
|
</p>
|
||||||
|
<h2 className="font-circular mb-4 text-lg font-normal leading-7 text-neutral-700 dark:text-neutral-300">
|
||||||
|
Top-up Credits
|
||||||
|
</h2>
|
||||||
|
<p className="font-circular mb-6 text-base font-normal leading-tight text-neutral-600 dark:text-neutral-400">
|
||||||
|
{topupStatus === "success" && (
|
||||||
|
<span className="text-green-500">
|
||||||
|
Your payment was successful. Your credits will be updated shortly.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{topupStatus === "cancel" && (
|
||||||
|
<span className="text-red-500">
|
||||||
|
Payment failed. Your payment method has not been charged.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="w-full">
|
||||||
|
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||||
|
1 USD = 100 credits, 5 USD is a minimum top-up
|
||||||
|
</label>
|
||||||
|
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="displayName"
|
||||||
|
value={amount}
|
||||||
|
placeholder="Top-up amount in USD"
|
||||||
|
min="5"
|
||||||
|
step="1"
|
||||||
|
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||||
|
onChange={(e) => setAmount(parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="default"
|
||||||
|
className="font-circular mt-4 h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||||
|
onClick={() => requestTopUp(amount)}
|
||||||
|
>
|
||||||
|
{"Top-up"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -33,14 +33,14 @@ export default function Page({}: {}) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching submissions:", error);
|
console.error("Error fetching submissions:", error);
|
||||||
}
|
}
|
||||||
}, [api, supabase]);
|
}, [api]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [supabase]);
|
}, [supabase, fetchData]);
|
||||||
|
|
||||||
const onEditSubmission = useCallback((submission: StoreSubmissionRequest) => {
|
const onEditSubmission = useCallback((submission: StoreSubmissionRequest) => {
|
||||||
setSubmissionData(submission);
|
setSubmissionData(submission);
|
||||||
|
@ -56,7 +56,7 @@ export default function Page({}: {}) {
|
||||||
api.deleteStoreSubmission(submission_id);
|
api.deleteStoreSubmission(submission_id);
|
||||||
fetchData();
|
fetchData();
|
||||||
},
|
},
|
||||||
[supabase],
|
[api, supabase, fetchData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onOpenPopout = useCallback(() => {
|
const onOpenPopout = useCallback(() => {
|
||||||
|
|
|
@ -98,6 +98,7 @@ export default function PrivatePage() {
|
||||||
// This contains ids for built-in "Use Credits for X" credentials
|
// This contains ids for built-in "Use Credits for X" credentials
|
||||||
const hiddenCredentials = useMemo(
|
const hiddenCredentials = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
"744fdc56-071a-4761-b5a5-0af0ce10a2b5", // Ollama
|
||||||
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
|
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
|
||||||
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
|
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
|
||||||
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
|
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
links: [
|
links: [
|
||||||
{ text: "Creator Dashboard", href: "/store/dashboard" },
|
{ text: "Creator Dashboard", href: "/store/dashboard" },
|
||||||
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
|
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
|
||||||
|
{ text: "Credits", href: "/store/credits" },
|
||||||
{ text: "Integrations", href: "/store/integrations" },
|
{ text: "Integrations", href: "/store/integrations" },
|
||||||
{ text: "API Keys", href: "/store/api_keys" },
|
{ text: "API Keys", href: "/store/api_keys" },
|
||||||
{ text: "Profile", href: "/store/profile" },
|
{ text: "Profile", href: "/store/profile" },
|
||||||
|
|
|
@ -61,7 +61,7 @@ function SearchResults({
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [searchTerm, sort]);
|
}, [api, searchTerm, sort]);
|
||||||
|
|
||||||
const agentsCount = agents.length;
|
const agentsCount = agents.length;
|
||||||
const creatorsCount = creators.length;
|
const creatorsCount = creators.length;
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="sticky top-0 z-50 mx-[16px] hidden h-16 w-full max-w-[1600px] items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
|
<nav className="sticky top-0 z-50 mx-[16px] hidden h-16 max-w-[1600px] items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
|
||||||
<div className="flex items-center gap-11">
|
<div className="flex items-center gap-11">
|
||||||
<div className="relative h-10 w-[88.87px]">
|
<div className="relative h-10 w-[88.87px]">
|
||||||
<IconAutoGPTLogo className="h-full w-full" />
|
<IconAutoGPTLogo className="h-full w-full" />
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IconIntegrations,
|
IconIntegrations,
|
||||||
IconProfile,
|
IconProfile,
|
||||||
IconSliders,
|
IconSliders,
|
||||||
|
IconCoin,
|
||||||
} from "../ui/icons";
|
} from "../ui/icons";
|
||||||
|
|
||||||
interface SidebarLinkGroup {
|
interface SidebarLinkGroup {
|
||||||
|
@ -22,6 +23,10 @@ interface SidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||||
|
const stripeAvailable = Boolean(
|
||||||
|
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sheet>
|
<Sheet>
|
||||||
|
@ -49,6 +54,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||||
Creator dashboard
|
Creator dashboard
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
{stripeAvailable && (
|
||||||
|
<Link
|
||||||
|
href="/store/credits"
|
||||||
|
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<IconCoin className="h-6 w-6" />
|
||||||
|
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||||
|
Credits
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href="/store/integrations"
|
href="/store/integrations"
|
||||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||||
|
@ -102,6 +118,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||||
Agent dashboard
|
Agent dashboard
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
{stripeAvailable && (
|
||||||
|
<Link
|
||||||
|
href="/store/credits"
|
||||||
|
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<IconCoin className="h-6 w-6" />
|
||||||
|
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||||
|
Credits
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href="/store/integrations"
|
href="/store/integrations"
|
||||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthCard({ children }: Props) {
|
export default function AuthCard({ children, className }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[80vh] w-[32rem] items-center justify-center">
|
<div className={cn("flex h-[80vh] w-[32rem] items-center justify-center", className)}>
|
||||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-md">
|
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-md">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,37 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { IconRefresh } from "@/components/ui/icons";
|
import { IconRefresh } from "@/components/ui/icons";
|
||||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
import useCredits from "@/hooks/useCredits";
|
||||||
|
|
||||||
export default function CreditButton() {
|
export default function CreditButton() {
|
||||||
const [credit, setCredit] = useState<number | null>(null);
|
const { credits, fetchCredits } = useCredits();
|
||||||
const api = useBackendAPI();
|
|
||||||
|
|
||||||
const fetchCredit = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.getUserCredit();
|
|
||||||
setCredit(response.credits);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching credit:", error);
|
|
||||||
setCredit(null);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCredit();
|
|
||||||
}, [fetchCredit]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
credit !== null && (
|
credits !== null && (
|
||||||
<Button
|
<Button
|
||||||
onClick={fetchCredit}
|
onClick={fetchCredits}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center space-x-2 rounded-xl bg-gray-200"
|
className="flex items-center space-x-2 rounded-xl bg-gray-200"
|
||||||
>
|
>
|
||||||
<span className="mr-2 flex items-center text-foreground">
|
<span className="mr-2 flex items-center text-foreground">
|
||||||
{credit} <span className="ml-2 text-muted-foreground"> credits</span>
|
{credits} <span className="ml-2 text-muted-foreground"> credits</span>
|
||||||
</span>
|
</span>
|
||||||
<IconRefresh />
|
<IconRefresh />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -313,8 +313,6 @@ export const NodeGenericInputField: FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("propSchema", propSchema);
|
|
||||||
|
|
||||||
if ("properties" in propSchema) {
|
if ("properties" in propSchema) {
|
||||||
// Render a multi-select for all-boolean sub-schemas with more than 3 properties
|
// Render a multi-select for all-boolean sub-schemas with more than 3 properties
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -323,7 +323,7 @@ export const IconCoin = createIcon((props) => (
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="1.25"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
aria-label="Coin Icon"
|
aria-label="Coin Icon"
|
||||||
|
|
|
@ -874,7 +874,7 @@ export default function useAgentGraph(
|
||||||
request: "save",
|
request: "save",
|
||||||
state: "saving",
|
state: "saving",
|
||||||
});
|
});
|
||||||
}, [saveAgent]);
|
}, [saveAgent, saveRunRequest.state]);
|
||||||
|
|
||||||
const requestSaveAndRun = useCallback(() => {
|
const requestSaveAndRun = useCallback(() => {
|
||||||
saveAgent();
|
saveAgent();
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(
|
||||||
|
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function useCredits(): {
|
||||||
|
credits: number | null;
|
||||||
|
fetchCredits: () => void;
|
||||||
|
requestTopUp: (usd_amount: number) => Promise<void>;
|
||||||
|
} {
|
||||||
|
const [credits, setCredits] = useState<number | null>(null);
|
||||||
|
const api = useMemo(() => new AutoGPTServerAPI(), []);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const fetchCredits = useCallback(async () => {
|
||||||
|
const response = await api.getUserCredit();
|
||||||
|
setCredits(response.credits);
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCredits();
|
||||||
|
}, [fetchCredits]);
|
||||||
|
|
||||||
|
const requestTopUp = useCallback(
|
||||||
|
async (usd_amount: number) => {
|
||||||
|
const stripe = await stripePromise;
|
||||||
|
|
||||||
|
if (!stripe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert dollar amount to credit count
|
||||||
|
const response = await api.requestTopUp(usd_amount * 100);
|
||||||
|
router.push(response.checkout_url);
|
||||||
|
},
|
||||||
|
[api, router],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
credits,
|
||||||
|
fetchCredits,
|
||||||
|
requestTopUp,
|
||||||
|
};
|
||||||
|
}
|
|
@ -88,6 +88,14 @@ export default class BackendAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestTopUp(amount: number): Promise<{ checkout_url: string }> {
|
||||||
|
return this._request("POST", "/credits", { amount });
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfillCheckout(): Promise<void> {
|
||||||
|
return this._request("PATCH", "/credits");
|
||||||
|
}
|
||||||
|
|
||||||
getBlocks(): Promise<Block[]> {
|
getBlocks(): Promise<Block[]> {
|
||||||
return this._get("/blocks");
|
return this._get("/blocks");
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,39 +42,75 @@ test.describe("Build", () => { //(1)!
|
||||||
});
|
});
|
||||||
// --8<-- [end:BuildPageExample]
|
// --8<-- [end:BuildPageExample]
|
||||||
|
|
||||||
test("user can add all blocks", async ({ page }, testInfo) => {
|
test("user can add all blocks a-l", async ({ page }, testInfo) => {
|
||||||
// this test is slow af so we 10x the timeout (sorry future me)
|
// this test is slow af so we 10x the timeout (sorry future me)
|
||||||
await test.setTimeout(testInfo.timeout * 10);
|
await test.setTimeout(testInfo.timeout * 100);
|
||||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||||
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||||
await buildPage.closeTutorial();
|
await buildPage.closeTutorial();
|
||||||
await buildPage.openBlocksPanel();
|
await buildPage.openBlocksPanel();
|
||||||
const blocks = await buildPage.getBlocks();
|
const blocks = await buildPage.getBlocks();
|
||||||
|
|
||||||
// add all the blocks in order
|
const blocksToSkip = await buildPage.getBlocksToSkip();
|
||||||
|
|
||||||
|
// add all the blocks in order except for the agent executor block
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
|
if (block.name[0].toLowerCase() >= "m") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!blocksToSkip.some((b) => b === block.id)) {
|
||||||
await buildPage.addBlock(block);
|
await buildPage.addBlock(block);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await buildPage.closeBlocksPanel();
|
await buildPage.closeBlocksPanel();
|
||||||
// check that all the blocks are visible
|
// check that all the blocks are visible
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
|
if (block.name[0].toLowerCase() >= "m") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!blocksToSkip.some((b) => b === block.id)) {
|
||||||
|
console.log("Checking block:", block.name);
|
||||||
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fill in the input for the agent input block
|
|
||||||
await buildPage.fillBlockInputByPlaceholder(
|
// check that we can save the agent with all the blocks
|
||||||
blocks.find((b) => b.name === "Agent Input")?.id ?? "",
|
await buildPage.saveAgent("all blocks test", "all blocks test");
|
||||||
"Enter Name",
|
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340
|
||||||
"Agent Input Field",
|
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||||
);
|
});
|
||||||
await buildPage.fillBlockInputByPlaceholder(
|
|
||||||
blocks.find((b) => b.name === "Agent Output")?.id ?? "",
|
test("user can add all blocks m-z", async ({ page }, testInfo) => {
|
||||||
"Enter Name",
|
// this test is slow af so we 10x the timeout (sorry future me)
|
||||||
"Agent Output Field",
|
await test.setTimeout(testInfo.timeout * 100);
|
||||||
);
|
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||||
|
await test.expect(page).toHaveURL(new RegExp("/.*build"));
|
||||||
|
await buildPage.closeTutorial();
|
||||||
|
await buildPage.openBlocksPanel();
|
||||||
|
const blocks = await buildPage.getBlocks();
|
||||||
|
|
||||||
|
const blocksToSkip = await buildPage.getBlocksToSkip();
|
||||||
|
|
||||||
|
// add all the blocks in order except for the agent executor block
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.name[0].toLowerCase() < "m") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!blocksToSkip.some((b) => b === block.id)) {
|
||||||
|
await buildPage.addBlock(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await buildPage.closeBlocksPanel();
|
||||||
|
// check that all the blocks are visible
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.name[0].toLowerCase() < "m") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!blocksToSkip.some((b) => b === block.id)) {
|
||||||
|
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check that we can save the agent with all the blocks
|
// check that we can save the agent with all the blocks
|
||||||
await buildPage.saveAgent("all blocks test", "all blocks test");
|
await buildPage.saveAgent("all blocks test", "all blocks test");
|
||||||
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340
|
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
// --8<-- [start:AttachAgentId]
|
// --8<-- [start:AttachAgentId]
|
||||||
|
test.describe("Monitor", () => {
|
||||||
test.describe.skip("Monitor", () => {
|
|
||||||
let buildPage: BuildPage;
|
let buildPage: BuildPage;
|
||||||
let monitorPage: MonitorPage;
|
let monitorPage: MonitorPage;
|
||||||
|
|
||||||
|
@ -54,21 +53,25 @@ test.describe.skip("Monitor", () => {
|
||||||
await test.expect(agents.length).toBeGreaterThan(0);
|
await test.expect(agents.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user can export and import agents", async ({
|
test.skip("user can export and import agents", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo: TestInfo) => {
|
}, testInfo: TestInfo) => {
|
||||||
// --8<-- [start:ReadAgentId]
|
// --8<-- [start:ReadAgentId]
|
||||||
if (testInfo.attachments.length === 0 || !testInfo.attachments[0].body) {
|
if (testInfo.attachments.length === 0 || !testInfo.attachments[0].body) {
|
||||||
throw new Error("No agent id attached to the test");
|
throw new Error("No agent id attached to the test");
|
||||||
}
|
}
|
||||||
const id = testInfo.attachments[0].body.toString();
|
const testAttachName = testInfo.attachments[0].body.toString();
|
||||||
// --8<-- [end:ReadAgentId]
|
// --8<-- [end:ReadAgentId]
|
||||||
const agents = await monitorPage.listAgents();
|
const agents = await monitorPage.listAgents();
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent("download");
|
const downloadPromise = page.waitForEvent("download");
|
||||||
await monitorPage.exportToFile(
|
const agent = agents.find(
|
||||||
agents.find((a: any) => a.id === id) || agents[0],
|
(a: any) => a.name === `test-agent-${testAttachName}`,
|
||||||
);
|
);
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error(`Agent ${testAttachName} not found`);
|
||||||
|
}
|
||||||
|
await monitorPage.exportToFile(agent);
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
|
|
||||||
// Wait for the download process to complete and save the downloaded file somewhere.
|
// Wait for the download process to complete and save the downloaded file somewhere.
|
||||||
|
@ -78,9 +81,6 @@ test.describe.skip("Monitor", () => {
|
||||||
console.log(`downloaded file to ${download.suggestedFilename()}`);
|
console.log(`downloaded file to ${download.suggestedFilename()}`);
|
||||||
await test.expect(download.suggestedFilename()).toBeDefined();
|
await test.expect(download.suggestedFilename()).toBeDefined();
|
||||||
// test-agent-uuid-v1.json
|
// test-agent-uuid-v1.json
|
||||||
if (id) {
|
|
||||||
await test.expect(download.suggestedFilename()).toContain(id);
|
|
||||||
}
|
|
||||||
await test.expect(download.suggestedFilename()).toContain("test-agent-");
|
await test.expect(download.suggestedFilename()).toContain("test-agent-");
|
||||||
await test.expect(download.suggestedFilename()).toContain("v1.json");
|
await test.expect(download.suggestedFilename()).toContain("v1.json");
|
||||||
|
|
||||||
|
@ -89,9 +89,9 @@ test.describe.skip("Monitor", () => {
|
||||||
const filesInFolder = await fs.readdir(
|
const filesInFolder = await fs.readdir(
|
||||||
`${monitorPage.downloadsFolder}/monitor`,
|
`${monitorPage.downloadsFolder}/monitor`,
|
||||||
);
|
);
|
||||||
const importFile = filesInFolder.find((f) => f.includes(id));
|
const importFile = filesInFolder.find((f) => f.includes(testAttachName));
|
||||||
if (!importFile) {
|
if (!importFile) {
|
||||||
throw new Error(`No import file found for agent ${id}`);
|
throw new Error(`No import file found for agent ${testAttachName}`);
|
||||||
}
|
}
|
||||||
const baseName = importFile.split(".")[0];
|
const baseName = importFile.split(".")[0];
|
||||||
await monitorPage.importFromFile(
|
await monitorPage.importFromFile(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ElementHandle, Locator, Page } from "@playwright/test";
|
import { ElementHandle, Locator, Page } from "@playwright/test";
|
||||||
import { BasePage } from "./base.page";
|
import { BasePage } from "./base.page";
|
||||||
|
|
||||||
interface Block {
|
export interface Block {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -378,6 +378,39 @@ export class BuildPage extends BasePage {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAgentExecutorBlockDetails(): Promise<Block> {
|
||||||
|
return {
|
||||||
|
id: "e189baac-8c20-45a1-94a7-55177ea42565",
|
||||||
|
name: "Agent Executor",
|
||||||
|
description: "Executes an existing agent inside your agent",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAgentOutputBlockDetails(): Promise<Block> {
|
||||||
|
return {
|
||||||
|
id: "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||||
|
name: "Agent Output",
|
||||||
|
description: "This block is used to output the result of an agent.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAgentInputBlockDetails(): Promise<Block> {
|
||||||
|
return {
|
||||||
|
id: "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||||
|
name: "Agent Input",
|
||||||
|
description: "This block is used to provide input to the graph.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGithubTriggerBlockDetails(): Promise<Block> {
|
||||||
|
return {
|
||||||
|
id: "6c60ec01-8128-419e-988f-96a063ee2fea",
|
||||||
|
name: "Github Trigger",
|
||||||
|
description:
|
||||||
|
"This block triggers on pull request events and outputs the event type and payload.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async nextTutorialStep(): Promise<void> {
|
async nextTutorialStep(): Promise<void> {
|
||||||
console.log(`clicking next tutorial step`);
|
console.log(`clicking next tutorial step`);
|
||||||
await this.page.getByRole("button", { name: "Next" }).click();
|
await this.page.getByRole("button", { name: "Next" }).click();
|
||||||
|
@ -448,6 +481,15 @@ export class BuildPage extends BasePage {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBlocksToSkip(): Promise<string[]> {
|
||||||
|
return [
|
||||||
|
(await this.getAgentExecutorBlockDetails()).id,
|
||||||
|
(await this.getAgentInputBlockDetails()).id,
|
||||||
|
(await this.getAgentOutputBlockDetails()).id,
|
||||||
|
(await this.getGithubTriggerBlockDetails()).id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
async waitForRunTutorialButton(): Promise<void> {
|
async waitForRunTutorialButton(): Promise<void> {
|
||||||
console.log(`waiting for run tutorial button`);
|
console.log(`waiting for run tutorial button`);
|
||||||
await this.page.waitForSelector('[id="press-run-label"]');
|
await this.page.waitForSelector('[id="press-run-label"]');
|
||||||
|
|
|
@ -43,9 +43,6 @@ export class MonitorPage extends BasePage {
|
||||||
async isLoaded(): Promise<boolean> {
|
async isLoaded(): Promise<boolean> {
|
||||||
console.log(`checking if monitor page is loaded`);
|
console.log(`checking if monitor page is loaded`);
|
||||||
try {
|
try {
|
||||||
// Wait for network to settle first
|
|
||||||
await this.page.waitForLoadState("networkidle", { timeout: 10_000 });
|
|
||||||
|
|
||||||
// Wait for the monitor page
|
// Wait for the monitor page
|
||||||
await this.page.getByTestId("monitor-page").waitFor({
|
await this.page.getByTestId("monitor-page").waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
|
@ -55,7 +52,7 @@ export class MonitorPage extends BasePage {
|
||||||
// Wait for table headers to be visible (indicates table structure is ready)
|
// Wait for table headers to be visible (indicates table structure is ready)
|
||||||
await this.page.locator("thead th").first().waitFor({
|
await this.page.locator("thead th").first().waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 5_000,
|
timeout: 15_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for either a table row or an empty tbody to be present
|
// Wait for either a table row or an empty tbody to be present
|
||||||
|
@ -63,14 +60,14 @@ export class MonitorPage extends BasePage {
|
||||||
// Wait for at least one row
|
// Wait for at least one row
|
||||||
this.page.locator("tbody tr[data-testid]").first().waitFor({
|
this.page.locator("tbody tr[data-testid]").first().waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 5_000,
|
timeout: 15_000,
|
||||||
}),
|
}),
|
||||||
// OR wait for an empty tbody (indicating no agents but table is loaded)
|
// OR wait for an empty tbody (indicating no agents but table is loaded)
|
||||||
this.page
|
this.page
|
||||||
.locator("tbody[data-testid='agent-flow-list-body']:empty")
|
.locator("tbody[data-testid='agent-flow-list-body']:empty")
|
||||||
.waitFor({
|
.waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 5_000,
|
timeout: 15_000,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -114,6 +111,13 @@ export class MonitorPage extends BasePage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
agents.reduce((acc, agent) => {
|
||||||
|
if (!agent.id.includes("flow-run")) {
|
||||||
|
acc.push(agent);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as Agent[]);
|
||||||
|
|
||||||
return agents;
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +223,7 @@ export class MonitorPage extends BasePage {
|
||||||
async exportToFile(agent: Agent) {
|
async exportToFile(agent: Agent) {
|
||||||
await this.clickAgent(agent.id);
|
await this.clickAgent(agent.id);
|
||||||
|
|
||||||
console.log(`exporting agent ${agent.id} ${agent.name} to file`);
|
console.log(`exporting agent id: ${agent.id} name: ${agent.name} to file`);
|
||||||
await this.page.getByTestId("export-button").click();
|
await this.page.getByTestId("export-button").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1693,10 +1693,10 @@
|
||||||
outvariant "^1.4.3"
|
outvariant "^1.4.3"
|
||||||
strict-event-emitter "^0.5.1"
|
strict-event-emitter "^0.5.1"
|
||||||
|
|
||||||
"@next/env@14.2.20":
|
"@next/env@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.20.tgz#0be2cc955f4eb837516e7d7382284cd5bc1d5a02"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.23.tgz#3003b53693cbc476710b856f83e623c8231a6be9"
|
||||||
integrity sha512-JfDpuOCB0UBKlEgEy/H6qcBSzHimn/YWjUHzKl1jMeUO+QVRdzmTTl8gFJaNO87c8DXmVKhFCtwxQ9acqB3+Pw==
|
integrity sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@15.1.3":
|
"@next/eslint-plugin-next@15.1.3":
|
||||||
version "15.1.3"
|
version "15.1.3"
|
||||||
|
@ -1705,50 +1705,50 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob "3.3.1"
|
fast-glob "3.3.1"
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@14.2.20":
|
"@next/swc-darwin-arm64@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.20.tgz#3c99d318c08362aedde5d2778eec3a50b8085d99"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz#6d83f03e35e163e8bbeaf5aeaa6bf55eed23d7a1"
|
||||||
integrity sha512-WDfq7bmROa5cIlk6ZNonNdVhKmbCv38XteVFYsxea1vDJt3SnYGgxLGMTXQNfs5OkFvAhmfKKrwe7Y0Hs+rWOg==
|
integrity sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@14.2.20":
|
"@next/swc-darwin-x64@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.20.tgz#fd547fad1446a677f29c1160006fdd482bba4052"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz#e02abc35d5e36ce1550f674f8676999f293ba54f"
|
||||||
integrity sha512-XIQlC+NAmJPfa2hruLvr1H1QJJeqOTDV+v7tl/jIdoFvqhoihvSNykLU/G6NMgoeo+e/H7p/VeWSOvMUHKtTIg==
|
integrity sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@14.2.20":
|
"@next/swc-linux-arm64-gnu@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.20.tgz#1d6ba1929d3a11b74c0185cdeca1e38b824222ca"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz#f13516ad2d665950951b59e7c239574bb8504d63"
|
||||||
integrity sha512-pnzBrHTPXIMm5QX3QC8XeMkpVuoAYOmyfsO4VlPn+0NrHraNuWjdhe+3xLq01xR++iCvX+uoeZmJDKcOxI201Q==
|
integrity sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@14.2.20":
|
"@next/swc-linux-arm64-musl@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.20.tgz#0fe0c67b5d916f99ca76b39416557af609768f17"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz#10d05a1c161dc8426d54ccf6d9bbed6953a3252a"
|
||||||
integrity sha512-WhJJAFpi6yqmUx1momewSdcm/iRXFQS0HU2qlUGlGE/+98eu7JWLD5AAaP/tkK1mudS/rH2f9E3WCEF2iYDydQ==
|
integrity sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@14.2.20":
|
"@next/swc-linux-x64-gnu@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.20.tgz#6d29fa8cdb6a9f8250c2048aaa24538f0cd0b02d"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz#7f5856df080f58ba058268b30429a2ab52500536"
|
||||||
integrity sha512-ao5HCbw9+iG1Kxm8XsGa3X174Ahn17mSYBQlY6VGsdsYDAbz/ZP13wSLfvlYoIDn1Ger6uYA+yt/3Y9KTIupRg==
|
integrity sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@14.2.20":
|
"@next/swc-linux-x64-musl@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.20.tgz#bfc57482bc033fda8455e8aab1c3cbc44f0c4690"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz#d494ebdf26421c91be65f9b1d095df0191c956d8"
|
||||||
integrity sha512-CXm/kpnltKTT7945np6Td3w7shj/92TMRPyI/VvveFe8+YE+/YOJ5hyAWK5rpx711XO1jBCgXl211TWaxOtkaA==
|
integrity sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@14.2.20":
|
"@next/swc-win32-arm64-msvc@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.20.tgz#6f7783e643310510240a981776532ffe0e02af95"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz#62786e7ba4822a20b6666e3e03e5a389b0e7eb3b"
|
||||||
integrity sha512-upJn2HGQgKNDbXVfIgmqT2BN8f3z/mX8ddoyi1I565FHbfowVK5pnMEwauvLvaJf4iijvuKq3kw/b6E9oIVRWA==
|
integrity sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@14.2.20":
|
"@next/swc-win32-ia32-msvc@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.20.tgz#58c7720687e80a13795e22c29d5860fa142e44fc"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz#ef028af91e1c40a4ebba0d2c47b23c1eeb299594"
|
||||||
integrity sha512-igQW/JWciTGJwj3G1ipalD2V20Xfx3ywQy17IV0ciOUBbFhNfyU1DILWsTi32c8KmqgIDviUEulW/yPb2FF90w==
|
integrity sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@14.2.20":
|
"@next/swc-win32-x64-msvc@14.2.23":
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.20.tgz#689bc7beb8005b73c95d926e7edfb7f73efc78f2"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz#c81838f02f2f16a321b7533890fb63c1edec68e1"
|
||||||
integrity sha512-AFmqeLW6LtxeFTuoB+MXFeM5fm5052i3MU6xD0WzJDOwku6SkZaxb1bxjBaRC8uNqTRTSPl0yMFtjNowIVI67w==
|
integrity sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==
|
||||||
|
|
||||||
"@next/third-parties@^15.1.3":
|
"@next/third-parties@^15.1.3":
|
||||||
version "15.1.3"
|
version "15.1.3"
|
||||||
|
@ -3257,6 +3257,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.4.7.tgz#c308f6a883999bd35e87826738ab8a76515932b5"
|
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.4.7.tgz#c308f6a883999bd35e87826738ab8a76515932b5"
|
||||||
integrity sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==
|
integrity sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==
|
||||||
|
|
||||||
|
"@stripe/stripe-js@^5.3.0":
|
||||||
|
version "5.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-5.4.0.tgz#847e870ddfe9283432526867857a4c1fba9b11ed"
|
||||||
|
integrity sha512-3tfMbSvLGB+OsJ2MsjWjWo+7sp29dwx+3+9kG/TEnZQJt+EwbF/Nomm43cSK+6oXZA9uhspgyrB+BbrPRumx4g==
|
||||||
|
|
||||||
"@supabase/auth-js@2.67.3":
|
"@supabase/auth-js@2.67.3":
|
||||||
version "2.67.3"
|
version "2.67.3"
|
||||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.3.tgz#a1f5eb22440b0cdbf87fe2ecae662a8dd8bb2028"
|
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.3.tgz#a1f5eb22440b0cdbf87fe2ecae662a8dd8bb2028"
|
||||||
|
@ -8976,12 +8981,12 @@ next-themes@^0.4.4:
|
||||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13"
|
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13"
|
||||||
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
|
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
|
||||||
|
|
||||||
next@^14.2.13:
|
next@^14.2.21:
|
||||||
version "14.2.20"
|
version "14.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.20.tgz#99b551d87ca6505ce63074904cb31a35e21dac9b"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.2.23.tgz#37edc9a4d42c135fd97a4092f829e291e2e7c943"
|
||||||
integrity sha512-yPvIiWsiyVYqJlSQxwmzMIReXn5HxFNq4+tlVQ812N1FbvhmE+fDpIAD7bcS2mGYQwPJ5vAsQouyme2eKsxaug==
|
integrity sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "14.2.20"
|
"@next/env" "14.2.23"
|
||||||
"@swc/helpers" "0.5.5"
|
"@swc/helpers" "0.5.5"
|
||||||
busboy "1.6.0"
|
busboy "1.6.0"
|
||||||
caniuse-lite "^1.0.30001579"
|
caniuse-lite "^1.0.30001579"
|
||||||
|
@ -8989,15 +8994,15 @@ next@^14.2.13:
|
||||||
postcss "8.4.31"
|
postcss "8.4.31"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-darwin-arm64" "14.2.20"
|
"@next/swc-darwin-arm64" "14.2.23"
|
||||||
"@next/swc-darwin-x64" "14.2.20"
|
"@next/swc-darwin-x64" "14.2.23"
|
||||||
"@next/swc-linux-arm64-gnu" "14.2.20"
|
"@next/swc-linux-arm64-gnu" "14.2.23"
|
||||||
"@next/swc-linux-arm64-musl" "14.2.20"
|
"@next/swc-linux-arm64-musl" "14.2.23"
|
||||||
"@next/swc-linux-x64-gnu" "14.2.20"
|
"@next/swc-linux-x64-gnu" "14.2.23"
|
||||||
"@next/swc-linux-x64-musl" "14.2.20"
|
"@next/swc-linux-x64-musl" "14.2.23"
|
||||||
"@next/swc-win32-arm64-msvc" "14.2.20"
|
"@next/swc-win32-arm64-msvc" "14.2.23"
|
||||||
"@next/swc-win32-ia32-msvc" "14.2.20"
|
"@next/swc-win32-ia32-msvc" "14.2.23"
|
||||||
"@next/swc-win32-x64-msvc" "14.2.20"
|
"@next/swc-win32-x64-msvc" "14.2.23"
|
||||||
|
|
||||||
no-case@^3.0.4:
|
no-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
|
|
Loading…
Reference in New Issue