mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
Add real-time balance tracking for PPQ provider
Implemented account balance display that updates after each query: Features: - Added getBalance() method to PPQ provider adapter - Fetches balance from https://api.ppq.ai/credits/balance - Displays balance in chat header with green formatting - Updates automatically after each message is sent - Shows loading spinner during balance refresh - Handles multiple response formats gracefully - Tooltip explains "updates after each query" UI Changes: - Balance shown as "Balance: $X.XX" in header - Green color for positive balance visibility - Positioned between title and token counter - Only shown when balance is available Technical Details: - POST request with Bearer token authentication - Flexible response parsing (balance/credits/amount fields) - Error handling with console warnings - Optional feature (gracefully handles providers without balance support) This helps users track their PPQ credits in real-time while chatting.
This commit is contained in:
@@ -170,6 +170,22 @@ export function LLMChatViewer({ conversationId }: LLMChatViewerProps) {
|
||||
|
||||
const [loadingState] = useState<ChatLoadingState>("success");
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [balance, setBalance] = useState<number | null>(null);
|
||||
const [loadingBalance, setLoadingBalance] = useState(false);
|
||||
|
||||
// Fetch balance when provider changes
|
||||
useEffect(() => {
|
||||
if (provider && provider.getBalance) {
|
||||
setLoadingBalance(true);
|
||||
provider
|
||||
.getBalance()
|
||||
.then((bal) => setBalance(bal))
|
||||
.catch(() => setBalance(null))
|
||||
.finally(() => setLoadingBalance(false));
|
||||
} else {
|
||||
setBalance(null);
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
// Update conversation model when provider changes
|
||||
useEffect(() => {
|
||||
@@ -274,6 +290,16 @@ export function LLMChatViewer({ conversationId }: LLMChatViewerProps) {
|
||||
totalCost: prev.totalCost + (response.cost || 0),
|
||||
};
|
||||
});
|
||||
|
||||
// Refresh balance after successful message
|
||||
if (provider.getBalance) {
|
||||
provider
|
||||
.getBalance()
|
||||
.then((bal) => setBalance(bal))
|
||||
.catch(() => {
|
||||
/* ignore errors */
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
// Add error message
|
||||
@@ -307,7 +333,30 @@ export function LLMChatViewer({ conversationId }: LLMChatViewerProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="size-4" />
|
||||
<span className="text-sm font-semibold">{conversation.title}</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* Balance Display */}
|
||||
{balance !== null && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-muted-foreground">Balance:</span>
|
||||
<span className="font-medium text-green-600 dark:text-green-400">
|
||||
${balance.toFixed(2)}
|
||||
</span>
|
||||
{loadingBalance && (
|
||||
<Loader2 className="size-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Account balance (updates after each query)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Token Counter */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -305,4 +305,47 @@ export class PPQProviderAdapter implements LLMProviderAdapter {
|
||||
// Rough estimate: ~4 characters per token
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
async getBalance(): Promise<number | null> {
|
||||
try {
|
||||
// PPQ balance endpoint
|
||||
// Documentation: https://ppq.ai/api-topups
|
||||
const response = await fetch(`${this.baseUrl}/credits/balance`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
// Try with empty body first - Bearer auth might be sufficient
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch balance: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle various possible response formats
|
||||
if (typeof data === "number") {
|
||||
return data;
|
||||
}
|
||||
if (data.balance !== undefined) {
|
||||
return data.balance;
|
||||
}
|
||||
if (data.credits !== undefined) {
|
||||
return data.credits;
|
||||
}
|
||||
if (data.amount !== undefined) {
|
||||
return data.amount;
|
||||
}
|
||||
|
||||
console.warn("Unknown balance response format:", data);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Failed to get balance:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +125,9 @@ export interface LLMProviderAdapter {
|
||||
* Count tokens in text
|
||||
*/
|
||||
countTokens?(text: string, model: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get account balance (if supported)
|
||||
*/
|
||||
getBalance?(): Promise<number | null>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user