From bf0f8e6da946858998aa604a17e8d62aca65639b Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Mon, 16 Mar 2026 18:10:57 +0530 Subject: [PATCH] Enhance billing and credits management in the application - Added new commands in `package.json` for building macOS x64 applications. - Updated `TOOLS.md` to reflect the latest tool statistics and improved formatting. - Refactored `BillingPanel` to include credit balance and team usage state management, with loading indicators and top-up functionality. - Integrated budget usage tracking in `Conversations` page, displaying budget status and a top-up call-to-action when funds are depleted. - Updated `creditsApi` to include new endpoints for fetching team usage and handling credit top-ups, with improved data structures for credit balance and transactions. - Simplified onboarding steps by removing the invite code step and adjusting the animation sequence. These changes improve user experience by providing better visibility into billing and usage, as well as streamlining the onboarding process. --- ai/TOOLS.md | 48 ++--- package.json | 2 + .../settings/panels/BillingPanel.tsx | 172 +++++++++++++----- src/pages/Conversations.tsx | 89 ++++++++- src/pages/onboarding/Onboarding.tsx | 18 +- src/services/api/creditsApi.ts | 69 +++++-- 6 files changed, 299 insertions(+), 99 deletions(-) diff --git a/ai/TOOLS.md b/ai/TOOLS.md index 0dd27cd7a..0ff0036e6 100644 --- a/ai/TOOLS.md +++ b/ai/TOOLS.md @@ -7,7 +7,6 @@ This document lists all available tools that AlphaHuman can use to interact with AlphaHuman has access to **4 tools** across **3 integrations** organized into **6 categories**. **Quick Statistics:** - - **Telegram**: 2 tools - **Notion**: 1 tools - **Gmail**: 1 tools @@ -74,7 +73,6 @@ This skill provides 1 tool for gmail integration. **Description**: Send an email via Gmail **Parameters**: - - **body** (string) **(required)**: Email body content - **subject** (string) **(required)**: Email subject line - **to** (string) **(required)**: Recipient email address @@ -82,11 +80,14 @@ This skill provides 1 tool for gmail integration. **Usage Context**: Available in all environments **Example**: - ```json { "tool": "send_email", - "parameters": { "body": "example_body", "subject": "example_subject", "to": "example_to" } + "parameters": { + "body": "example_body", + "subject": "example_subject", + "to": "example_to" + } } ``` @@ -103,7 +104,6 @@ This skill provides 1 tool for notion integration. **Description**: Create a new page in Notion workspace **Parameters**: - - **content** (array): Page content blocks - **parent_id** (string) **(required)**: Parent database or page ID - **title** (string) **(required)**: Page title @@ -111,11 +111,14 @@ This skill provides 1 tool for notion integration. **Usage Context**: Available in all environments **Example**: - ```json { "tool": "create_page", - "parameters": { "content": [], "parent_id": "example_parent_id", "title": "example_title" } + "parameters": { + "content": [], + "parent_id": "example_parent_id", + "title": "example_title" + } } ``` @@ -132,7 +135,6 @@ This skill provides 2 tools for telegram integration. **Description**: Send a message to a Telegram chat or user **Parameters**: - - **chat_id** (string) **(required)**: Telegram chat ID or username - **message** (string) **(required)**: Message text to send - **parse_mode** (string): Message formatting mode @@ -140,7 +142,6 @@ This skill provides 2 tools for telegram integration. **Usage Context**: Available in all environments **Example**: - ```json { "tool": "send_message", @@ -159,16 +160,20 @@ This skill provides 2 tools for telegram integration. **Description**: Retrieve message history from a Telegram chat **Parameters**: - - **chat_id** (string) **(required)**: Telegram chat ID or username - **limit** (number): Number of messages to retrieve **Usage Context**: Available in all environments **Example**: - ```json -{ "tool": "get_chat_history", "parameters": { "chat_id": "example_chat_id", "limit": 10 } } +{ + "tool": "get_chat_history", + "parameters": { + "chat_id": "example_chat_id", + "limit": 10 + } +} ``` --- @@ -176,42 +181,36 @@ This skill provides 2 tools for telegram integration. ## Tool Usage Guidelines ### Authentication - - All tools require proper authentication setup through the Skills system - OAuth credentials are managed securely and refreshed automatically - API keys are stored encrypted in the application keychain - Test credentials are available for development and testing environments ### Rate Limiting - - Tools automatically respect API rate limits of external services - Intelligent retry logic handles temporary failures with exponential backoff - Bulk operations are automatically chunked to avoid hitting limits - Rate limit status is monitored and reported in real-time ### Error Handling - - All tools return structured error responses with detailed information - Network failures trigger automatic retry with configurable attempts - Invalid parameters return clear validation messages with examples - Tool execution timeouts are handled gracefully with partial results ### Security & Privacy - - Input validation is performed on all parameters using JSON Schema - Output sanitization prevents injection attacks and data leakage - Sensitive data is never logged or exposed in error messages - All API communications use secure protocols (HTTPS/TLS) ### Performance Optimization - - Tool results are cached when appropriate to reduce API calls - Parallel execution is used for independent operations - Connection pooling minimizes overhead for repeated API calls - Background sync keeps data fresh without blocking operations ### Monitoring & Observability - - Tool execution metrics are collected for performance analysis - Error rates and response times are monitored continuously - Debug logging is available in development environments @@ -230,21 +229,18 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V ## Integration Architecture ### V8 Runtime - - Skills execute in isolated V8 JavaScript contexts on desktop platforms - Mobile platforms use lightweight alternatives with server-side execution - Memory limits and execution timeouts prevent resource exhaustion - Inter-skill communication is managed through secure message passing ### API Bridge - - Tools communicate with external services through standardized API bridges - Rate limiting, retry logic, and error handling are implemented at the bridge level - Authentication tokens are managed centrally and shared across tools - Response caching and optimization are handled transparently ### Data Flow - 1. Tool request received from AI agent 2. Input validation and parameter processing 3. Skill execution in secure V8 context @@ -255,21 +251,18 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V ## Support & Troubleshooting ### Common Issues - 1. **Tool Not Available**: Check if the associated skill is enabled in Settings → Skills 2. **Authentication Errors**: Verify credentials in the skill's configuration panel 3. **Rate Limit Exceeded**: Wait for the limit to reset or upgrade your API plan 4. **Invalid Parameters**: Review the parameter documentation and examples above ### Getting Help - - **Skill Documentation**: Each skill has detailed setup and usage instructions - **Debug Logs**: Enable verbose logging in development mode for detailed error information - **Community Support**: Join our Discord community for help from other users - **Technical Support**: Contact our support team for critical issues ### Contributing - - **New Tools**: Submit tool requests through our GitHub repository - **Bug Reports**: Report issues with specific tools and include error logs - **Improvements**: Suggest enhancements to existing tools and their documentation @@ -277,11 +270,10 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V --- **Tool Statistics** - - Total Tools: 4 - Active Skills: 3 - Categories: 6 -- Last Updated: 2026-03-11T23:23:47.633Z +- Last Updated: 2026-03-16T12:17:45.377Z -_This file was automatically generated at build time from the V8 skills runtime._ -_For the most up-to-date information, regenerate this file by running `yarn tools:generate`._ +*This file was automatically generated at build time from the V8 skills runtime.* +*For the most up-to-date information, regenerate this file by running `yarn tools:generate`.* diff --git a/package.json b/package.json index bf986b8f7..dfdd1b3c2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "tauri": "tauri", "tauri:dev": "RUST_LOG=debug source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-$(uname -m | sed 's/arm64/aarch64/; s/x86_64/x86_64/') tauri dev", "macos:build:debug": "yarn macos:build:release --debug", + "macos:x64:build:debug": "yarn macos:x64:build:release --debug", + "macos:x64:build:release": "source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-x86_64 tauri build --target x86_64-apple-darwin --bundles app dmg", "macos:build:release": "source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-$(uname -m | sed 's/arm64/aarch64/; s/x86_64/x86_64/') tauri build --bundles app dmg", "macos:build:sign:release": "source scripts/load-env.sh && tauri build --bundles app dmg", "macos:run": "open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'", diff --git a/src/components/settings/panels/BillingPanel.tsx b/src/components/settings/panels/BillingPanel.tsx index 0d704211b..443dcfd43 100644 --- a/src/components/settings/panels/BillingPanel.tsx +++ b/src/components/settings/panels/BillingPanel.tsx @@ -1,6 +1,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { billingApi } from '../../../services/api/billingApi'; +import { + type CreditBalance, + type TeamUsage, + creditsApi, +} from '../../../services/api/creditsApi'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { fetchCurrentUser } from '../../../store/userSlice'; import type { PlanTier } from '../../../types/api'; @@ -31,7 +36,12 @@ const BillingPanel = () => { const currentTier: PlanTier = activeTeam?.team.subscription?.plan ?? 'FREE'; const hasActive = activeTeam?.team.subscription?.hasActiveSubscription ?? false; const planExpiry = activeTeam?.team.subscription?.planExpiry; - const usage = user?.usage; + + // Credits & usage state + const [creditBalance, setCreditBalance] = useState(null); + const [teamUsage, setTeamUsage] = useState(null); + const [isLoadingCredits, setIsLoadingCredits] = useState(false); + const [isToppingUp, setIsToppingUp] = useState(false); // Local state const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly'); @@ -43,9 +53,18 @@ const BillingPanel = () => { const pollStartRef = useRef(0); const timeoutRef = useRef(null); - // Fetch current plan on mount + // Fetch current plan, credits balance, and team usage on mount useEffect(() => { billingApi.getCurrentPlan().catch(console.error); + + setIsLoadingCredits(true); + Promise.all([creditsApi.getBalance(), creditsApi.getTeamUsage()]) + .then(([balance, usage]) => { + setCreditBalance(balance); + setTeamUsage(usage); + }) + .catch(console.error) + .finally(() => setIsLoadingCredits(false)); }, []); // When crypto is selected, force annual @@ -102,7 +121,6 @@ const BillingPanel = () => { currentTierRef.current = currentTier; }, [currentTier]); - // eslint-disable-next-line react-hooks/preserve-manual-memoization const startPolling = useCallback(() => { if (pollRef.current) clearInterval(pollRef.current); pollStartRef.current = Date.now(); @@ -162,6 +180,18 @@ const BillingPanel = () => { } }; + const handleTopUp = async (amountUsd: number) => { + setIsToppingUp(true); + try { + const result = await creditsApi.topUp(amountUsd, 'stripe'); + await openUrl(result.url); + } catch (err) { + console.error('Top-up failed:', err); + } finally { + setIsToppingUp(false); + } + }; + // ── JSX ───────────────────────────────────────────────────────────── return (
@@ -174,40 +204,22 @@ const BillingPanel = () => { {/*
*/}
-
-
+
+ {/* ── Current Plan Header ───────────────────────────────── */} +

- Your Current Plan {currentTier} + Current Plan — {currentTier}

- {usage && ( - - {Math.round((usage.spentThisCycleUsd / usage.cycleBudgetUsd) * 100)}% used - - )} -
- - {hasActive && ( -
- {planExpiry && ( -

- Renews{' '} - {new Date(planExpiry).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - })} -

- )} + {hasActive && ( -
- )} - {/* Renewal date (for non-active subscriptions) */} - {!hasActive && planExpiry && ( + )} +
+ {planExpiry && (

Renews{' '} {new Date(planExpiry).toLocaleDateString('en-US', { @@ -217,20 +229,96 @@ const BillingPanel = () => { })}

)} - {usage && ( -
-
-
+
+ + {/* ── Inference Budget (Team Usage) ─────────────────────── */} +
+
+

Inference Budget

+ {isLoadingCredits && ( + Loading… + )} + {teamUsage && !isLoadingCredits && ( + + ${teamUsage.remainingUsd.toFixed(2)} / ${teamUsage.cycleBudgetUsd.toFixed(2)} remaining + + )} +
+ {teamUsage ? ( + <> +
+
+
+
+ + Daily usage: ${teamUsage.dailyUsage.toFixed(3)} + + + {((teamUsage.totalInputTokensThisCycle + teamUsage.totalOutputTokensThisCycle) / 1000).toFixed(1)}k tokens this cycle + +
+ {teamUsage.remainingUsd <= 0 && ( +

+ Budget exhausted — top up your credits to continue using AI features. +

+ )} + + ) : isLoadingCredits ? ( +
+ ) : ( +

Unable to load usage data

)}
+ + {/* ── Credits Balance & Top-up ──────────────────────────── */} +
+

Credits Balance

+ {creditBalance ? ( +
+
+ General credits + + ${creditBalance.balanceUsd.toFixed(2)} + +
+
+ Top-up credits + + ${creditBalance.topUpBalanceUsd.toFixed(2)} + +
+
+ ) : isLoadingCredits ? ( +
+
+
+
+ ) : ( +

Unable to load balance

+ )} +
+ {[5, 10, 25].map(amount => ( + + ))} +
+
{/* ── Interval toggle ──────────────────────────────────── */} diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index f43fbce55..297ea8c6c 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -18,6 +18,7 @@ import { type ModelInfo, type Tool, } from '../services/api/inferenceApi'; +import { type TeamUsage, creditsApi } from '../services/api/creditsApi'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import type { NotionPageSummary, NotionSummary, NotionUserProfile } from '../store/notionSlice'; import { @@ -141,6 +142,10 @@ const Conversations = () => { const [isLoadingModels, setIsLoadingModels] = useState(false); const [isSending, setIsSending] = useState(false); const [sendError, setSendError] = useState(null); + // Budget state + const [teamUsage, setTeamUsage] = useState(null); + const [isLoadingBudget, setIsLoadingBudget] = useState(false); + const isDragging = useRef(false); const messagesEndRef = useRef(null); const lastPanelWidthRef = useRef(panelWidth); @@ -219,6 +224,18 @@ const Conversations = () => { .finally(() => setIsLoadingModels(false)); }, []); + // Fetch inference budget on mount + useEffect(() => { + setIsLoadingBudget(true); + creditsApi + .getTeamUsage() + .then(data => setTeamUsage(data)) + .catch(() => { + // Budget unavailable — silently ignore + }) + .finally(() => setIsLoadingBudget(false)); + }, []); + // Remove thread fetching - threads are now loaded from Redux persist // Sync URL → Redux: when URL has a threadId param, select that thread @@ -995,6 +1012,34 @@ const Conversations = () => { {/* Message Input */}
+ {/* Budget depleted banner — show top-up CTA */} + {teamUsage && teamUsage.remainingUsd <= 0 && ( +
+
+ + + +

+ Daily inference budget exhausted. Top up to continue. +

+
+ +
+ )} + {/* Show warning if another thread is active */} {activeThreadId && activeThreadId !== selectedThreadId && (
@@ -1004,7 +1049,7 @@ const Conversations = () => {

)} - {/* Model selector */} + {/* Model selector + budget indicator */}
{isLoadingModels ? ( Loading models… @@ -1030,6 +1075,48 @@ const Conversations = () => { )} +
+ {/* Budget indicator — circular */} + {(isLoadingBudget || teamUsage) && (() => { + const size = 22; + const r = 9; + const circ = 2 * Math.PI * r; + const pct = teamUsage + ? Math.min(1, teamUsage.remainingUsd / teamUsage.cycleBudgetUsd) + : 0; + const dash = pct * circ; + return ( +
+ + + {teamUsage ? ( + + ) : ( + + )} + + {teamUsage && ( + + ${teamUsage.remainingUsd.toFixed(2)} + + )} +
+ ); + })()}
{sendError && (
diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 7d53f9060..8bbf56bf8 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -8,7 +8,6 @@ import { setOnboardedForUser } from '../../store/authSlice'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import FeaturesStep from './steps/FeaturesStep'; import GetStartedStep from './steps/GetStartedStep'; -import InviteCodeStep from './steps/InviteCodeStep'; import PrivacyStep from './steps/PrivacyStep'; const Onboarding = () => { @@ -16,14 +15,13 @@ const Onboarding = () => { const dispatch = useAppDispatch(); const user = useAppSelector(state => state.user.user); const [currentStep, setCurrentStep] = useState(0); - const totalSteps = 4; + const totalSteps = 3; // Lottie animation files for each step const stepAnimations = [ - '/lottie/trophy.json', // Step 1 - Invite Code - '/lottie/wave.json', // Step 2 - Features - '/lottie/safe3.json', // Step 3 - Privacy - '/lottie/trophy.json', // Step 4 - Get Started + '/lottie/wave.json', // Step 1 - Features + '/lottie/safe3.json', // Step 2 - Privacy + '/lottie/trophy.json', // Step 3 - Get Started ]; const handleNext = () => { @@ -54,15 +52,11 @@ const Onboarding = () => { const renderStep = () => { switch (currentStep) { case 1: - return ; - case 2: - return ; - case 3: return ; - case 4: + case 2: return ; default: - return ; + return ; } }; diff --git a/src/services/api/creditsApi.ts b/src/services/api/creditsApi.ts index 05375a5a6..a29002f4e 100644 --- a/src/services/api/creditsApi.ts +++ b/src/services/api/creditsApi.ts @@ -1,25 +1,38 @@ import type { ApiResponse } from '../../types/api'; import { apiClient } from '../apiClient'; -interface CreditBalance { - balance: number; - currency: string; +export interface CreditBalance { + balanceUsd: number; + topUpBalanceUsd: number; } -interface CreditTransaction { +export interface TeamUsage { + remainingUsd: number; + cycleBudgetUsd: number; + dailyUsage: number; + totalInputTokensThisCycle: number; + totalOutputTokensThisCycle: number; +} + +export interface TopUpResult { + url: string; + gatewayTransactionId: string; + amountUsd: number; + gateway: string; +} + +export interface CreditTransaction { id: string; - amount: number; - type: 'credit' | 'debit'; - description: string; + type: 'EARN' | 'SPEND'; + action: string; + amountUsd: number; + balanceAfterUsd: number; createdAt: string; - // Add other fields based on backend schema } -interface PaginatedTransactions { +export interface PaginatedTransactions { transactions: CreditTransaction[]; - totalCount: number; - page: number; - limit: number; + total: number; } /** @@ -27,11 +40,35 @@ interface PaginatedTransactions { */ export const creditsApi = { /** - * Get the current user's credit balance + * Get the current user's credit balance (general + top-up) * GET /credits/balance */ getBalance: async (): Promise => { - const response = await apiClient.get>('/credits/balance'); + const response = await apiClient.get>('/payments/credits/balance'); + return response.data; + }, + + /** + * Get team inference budget usage for the current billing cycle + * GET /teams/me/usage + */ + getTeamUsage: async (): Promise => { + const response = await apiClient.get>('/teams/me/usage'); + return response.data; + }, + + /** + * Start a top-up (get Stripe or Coinbase payment URL) + * POST /credits/top-up + */ + topUp: async ( + amountUsd: number, + gateway: 'stripe' | 'coinbase' = 'stripe' + ): Promise => { + const response = await apiClient.post>('/payments/credits/top-up', { + amountUsd, + gateway, + }); return response.data; }, @@ -39,9 +76,9 @@ export const creditsApi = { * Get paginated credit transaction history * GET /credits/transactions */ - getTransactions: async (page = 1, limit = 50): Promise => { + getTransactions: async (limit = 20, offset = 0): Promise => { const response = await apiClient.get>( - `/credits/transactions?page=${page}&limit=${limit}` + `/credits/transactions?limit=${limit}&offset=${offset}` ); return response.data; },