mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Feat: Add z.ai usage tracking
This commit is contained in:
@@ -81,6 +81,7 @@ export function BoardHeader({
|
||||
(state) => state.setAddFeatureUseSelectedWorktreeBranch
|
||||
);
|
||||
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
|
||||
const zaiAuthStatus = useSetupStore((state) => state.zaiAuthStatus);
|
||||
|
||||
// Worktree panel visibility (per-project)
|
||||
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
|
||||
@@ -112,6 +113,9 @@ export function BoardHeader({
|
||||
// Show if Codex is authenticated (CLI or API key)
|
||||
const showCodexUsage = !!codexAuthStatus?.authenticated;
|
||||
|
||||
// z.ai usage tracking visibility logic
|
||||
const showZaiUsage = !!zaiAuthStatus?.authenticated;
|
||||
|
||||
// State for mobile actions panel
|
||||
const [showActionsPanel, setShowActionsPanel] = useState(false);
|
||||
const [isRefreshingBoard, setIsRefreshingBoard] = useState(false);
|
||||
@@ -158,8 +162,10 @@ export function BoardHeader({
|
||||
<TooltipContent side="bottom">Refresh board state from server</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
|
||||
{isMounted && !isTablet && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
|
||||
{/* Usage Popover - show if any provider is authenticated, only on desktop */}
|
||||
{isMounted && !isTablet && (showClaudeUsage || showCodexUsage || showZaiUsage) && (
|
||||
<UsagePopover />
|
||||
)}
|
||||
|
||||
{/* Tablet/Mobile view: show hamburger menu with all controls */}
|
||||
{isMounted && isTablet && (
|
||||
@@ -178,6 +184,7 @@ export function BoardHeader({
|
||||
onOpenPlanDialog={onOpenPlanDialog}
|
||||
showClaudeUsage={showClaudeUsage}
|
||||
showCodexUsage={showCodexUsage}
|
||||
showZaiUsage={showZaiUsage}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ interface HeaderMobileMenuProps {
|
||||
// Usage bar visibility
|
||||
showClaudeUsage: boolean;
|
||||
showCodexUsage: boolean;
|
||||
showZaiUsage?: boolean;
|
||||
}
|
||||
|
||||
export function HeaderMobileMenu({
|
||||
@@ -47,18 +48,23 @@ export function HeaderMobileMenu({
|
||||
onOpenPlanDialog,
|
||||
showClaudeUsage,
|
||||
showCodexUsage,
|
||||
showZaiUsage = false,
|
||||
}: HeaderMobileMenuProps) {
|
||||
return (
|
||||
<>
|
||||
<HeaderActionsPanelTrigger isOpen={isOpen} onToggle={onToggle} />
|
||||
<HeaderActionsPanel isOpen={isOpen} onClose={onToggle} title="Board Controls">
|
||||
{/* Usage Bar - show if either provider is authenticated */}
|
||||
{(showClaudeUsage || showCodexUsage) && (
|
||||
{/* Usage Bar - show if any provider is authenticated */}
|
||||
{(showClaudeUsage || showCodexUsage || showZaiUsage) && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Usage
|
||||
</span>
|
||||
<MobileUsageBar showClaudeUsage={showClaudeUsage} showCodexUsage={showCodexUsage} />
|
||||
<MobileUsageBar
|
||||
showClaudeUsage={showClaudeUsage}
|
||||
showCodexUsage={showCodexUsage}
|
||||
showZaiUsage={showZaiUsage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { cn } from '@/lib/utils';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { AnthropicIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { AnthropicIcon, OpenAIIcon, ZaiIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
interface MobileUsageBarProps {
|
||||
showClaudeUsage: boolean;
|
||||
showCodexUsage: boolean;
|
||||
showZaiUsage?: boolean;
|
||||
}
|
||||
|
||||
// Helper to get progress bar color based on percentage
|
||||
@@ -18,15 +19,51 @@ function getProgressBarColor(percentage: number): string {
|
||||
return 'bg-green-500';
|
||||
}
|
||||
|
||||
// Helper to format large numbers with K/M suffixes
|
||||
function formatNumber(num: number): string {
|
||||
if (num >= 1_000_000_000) {
|
||||
return `${(num / 1_000_000_000).toFixed(1)}B`;
|
||||
}
|
||||
if (num >= 1_000_000) {
|
||||
return `${(num / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
if (num >= 1_000) {
|
||||
return `${(num / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
// Helper to format reset time
|
||||
function formatResetTime(unixTimestamp: number, isMilliseconds = false): string {
|
||||
const date = new Date(isMilliseconds ? unixTimestamp : unixTimestamp * 1000);
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
|
||||
if (diff < 3600000) {
|
||||
const mins = Math.ceil(diff / 60000);
|
||||
return `Resets in ${mins}m`;
|
||||
}
|
||||
if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const mins = Math.ceil((diff % 3600000) / 60000);
|
||||
return `Resets in ${hours}h${mins > 0 ? ` ${mins}m` : ''}`;
|
||||
}
|
||||
return `Resets ${date.toLocaleDateString()}`;
|
||||
}
|
||||
|
||||
// Individual usage bar component
|
||||
function UsageBar({
|
||||
label,
|
||||
percentage,
|
||||
isStale,
|
||||
details,
|
||||
resetText,
|
||||
}: {
|
||||
label: string;
|
||||
percentage: number;
|
||||
isStale: boolean;
|
||||
details?: string;
|
||||
resetText?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-1.5 first:mt-0">
|
||||
@@ -58,6 +95,14 @@ function UsageBar({
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{(details || resetText) && (
|
||||
<div className="flex items-center justify-between mt-0.5">
|
||||
{details && <span className="text-[9px] text-muted-foreground">{details}</span>}
|
||||
{resetText && (
|
||||
<span className="text-[9px] text-muted-foreground ml-auto">{resetText}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,16 +148,23 @@ function UsageItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileUsageBar({ showClaudeUsage, showCodexUsage }: MobileUsageBarProps) {
|
||||
export function MobileUsageBar({
|
||||
showClaudeUsage,
|
||||
showCodexUsage,
|
||||
showZaiUsage = false,
|
||||
}: MobileUsageBarProps) {
|
||||
const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore();
|
||||
const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore();
|
||||
const { zaiUsage, zaiUsageLastUpdated, setZaiUsage } = useAppStore();
|
||||
const [isClaudeLoading, setIsClaudeLoading] = useState(false);
|
||||
const [isCodexLoading, setIsCodexLoading] = useState(false);
|
||||
const [isZaiLoading, setIsZaiLoading] = useState(false);
|
||||
|
||||
// Check if data is stale (older than 2 minutes)
|
||||
const isClaudeStale =
|
||||
!claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000;
|
||||
const isCodexStale = !codexUsageLastUpdated || Date.now() - codexUsageLastUpdated > 2 * 60 * 1000;
|
||||
const isZaiStale = !zaiUsageLastUpdated || Date.now() - zaiUsageLastUpdated > 2 * 60 * 1000;
|
||||
|
||||
const fetchClaudeUsage = useCallback(async () => {
|
||||
setIsClaudeLoading(true);
|
||||
@@ -146,6 +198,22 @@ export function MobileUsageBar({ showClaudeUsage, showCodexUsage }: MobileUsageB
|
||||
}
|
||||
}, [setCodexUsage]);
|
||||
|
||||
const fetchZaiUsage = useCallback(async () => {
|
||||
setIsZaiLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.zai) return;
|
||||
const data = await api.zai.getUsage();
|
||||
if (!('error' in data)) {
|
||||
setZaiUsage(data);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail - usage display is optional
|
||||
} finally {
|
||||
setIsZaiLoading(false);
|
||||
}
|
||||
}, [setZaiUsage]);
|
||||
|
||||
const getCodexWindowLabel = (durationMins: number) => {
|
||||
if (durationMins < 60) return `${durationMins}m Window`;
|
||||
if (durationMins < 1440) return `${Math.round(durationMins / 60)}h Window`;
|
||||
@@ -165,8 +233,14 @@ export function MobileUsageBar({ showClaudeUsage, showCodexUsage }: MobileUsageB
|
||||
}
|
||||
}, [showCodexUsage, isCodexStale, fetchCodexUsage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showZaiUsage && isZaiStale) {
|
||||
fetchZaiUsage();
|
||||
}
|
||||
}, [showZaiUsage, isZaiStale, fetchZaiUsage]);
|
||||
|
||||
// Don't render if there's nothing to show
|
||||
if (!showClaudeUsage && !showCodexUsage) {
|
||||
if (!showClaudeUsage && !showCodexUsage && !showZaiUsage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -227,6 +301,45 @@ export function MobileUsageBar({ showClaudeUsage, showCodexUsage }: MobileUsageB
|
||||
)}
|
||||
</UsageItem>
|
||||
)}
|
||||
|
||||
{showZaiUsage && (
|
||||
<UsageItem icon={ZaiIcon} label="z.ai" isLoading={isZaiLoading} onRefresh={fetchZaiUsage}>
|
||||
{zaiUsage?.quotaLimits && (zaiUsage.quotaLimits.tokens || zaiUsage.quotaLimits.mcp) ? (
|
||||
<>
|
||||
{zaiUsage.quotaLimits.tokens && (
|
||||
<UsageBar
|
||||
label="Tokens"
|
||||
percentage={zaiUsage.quotaLimits.tokens.usedPercent}
|
||||
isStale={isZaiStale}
|
||||
details={`${formatNumber(zaiUsage.quotaLimits.tokens.used)} / ${formatNumber(zaiUsage.quotaLimits.tokens.limit)}`}
|
||||
resetText={
|
||||
zaiUsage.quotaLimits.tokens.nextResetTime
|
||||
? formatResetTime(zaiUsage.quotaLimits.tokens.nextResetTime, true)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{zaiUsage.quotaLimits.mcp && (
|
||||
<UsageBar
|
||||
label="MCP"
|
||||
percentage={zaiUsage.quotaLimits.mcp.usedPercent}
|
||||
isStale={isZaiStale}
|
||||
details={`${formatNumber(zaiUsage.quotaLimits.mcp.used)} / ${formatNumber(zaiUsage.quotaLimits.mcp.limit)} calls`}
|
||||
resetText={
|
||||
zaiUsage.quotaLimits.mcp.nextResetTime
|
||||
? formatResetTime(zaiUsage.quotaLimits.mcp.nextResetTime, true)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : zaiUsage ? (
|
||||
<p className="text-[10px] text-muted-foreground italic">No usage data from z.ai API</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground italic">Loading usage data...</p>
|
||||
)}
|
||||
</UsageItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// @ts-nocheck - API key management state with validation and persistence
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore, type ZaiAuthMethod } from '@/store/setup-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
|
||||
const logger = createLogger('ApiKeyManagement');
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
@@ -16,6 +20,7 @@ interface ApiKeyStatus {
|
||||
hasAnthropicKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
hasOpenaiKey: boolean;
|
||||
hasZaiKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,16 +29,20 @@ interface ApiKeyStatus {
|
||||
*/
|
||||
export function useApiKeyManagement() {
|
||||
const { apiKeys, setApiKeys } = useAppStore();
|
||||
const { setZaiAuthStatus } = useSetupStore();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// API key values
|
||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
|
||||
const [zaiKey, setZaiKey] = useState(apiKeys.zai);
|
||||
|
||||
// Visibility toggles
|
||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
||||
const [showGoogleKey, setShowGoogleKey] = useState(false);
|
||||
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
|
||||
const [showZaiKey, setShowZaiKey] = useState(false);
|
||||
|
||||
// Test connection states
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
@@ -42,6 +51,8 @@ export function useApiKeyManagement() {
|
||||
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(null);
|
||||
const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false);
|
||||
const [openaiTestResult, setOpenaiTestResult] = useState<TestResult | null>(null);
|
||||
const [testingZaiConnection, setTestingZaiConnection] = useState(false);
|
||||
const [zaiTestResult, setZaiTestResult] = useState<TestResult | null>(null);
|
||||
|
||||
// API key status from environment
|
||||
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
|
||||
@@ -54,6 +65,7 @@ export function useApiKeyManagement() {
|
||||
setAnthropicKey(apiKeys.anthropic);
|
||||
setGoogleKey(apiKeys.google);
|
||||
setOpenaiKey(apiKeys.openai);
|
||||
setZaiKey(apiKeys.zai);
|
||||
}, [apiKeys]);
|
||||
|
||||
// Check API key status from environment on mount
|
||||
@@ -68,6 +80,7 @@ export function useApiKeyManagement() {
|
||||
hasAnthropicKey: status.hasAnthropicKey,
|
||||
hasGoogleKey: status.hasGoogleKey,
|
||||
hasOpenaiKey: status.hasOpenaiKey,
|
||||
hasZaiKey: status.hasZaiKey || false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -173,13 +186,89 @@ export function useApiKeyManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
// Test z.ai connection
|
||||
const handleTestZaiConnection = async () => {
|
||||
setTestingZaiConnection(true);
|
||||
setZaiTestResult(null);
|
||||
|
||||
// Validate input first
|
||||
if (!zaiKey || zaiKey.trim().length === 0) {
|
||||
setZaiTestResult({
|
||||
success: false,
|
||||
message: 'Please enter an API key to test.',
|
||||
});
|
||||
setTestingZaiConnection(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
// Use the verify endpoint to test the key without storing it
|
||||
const response = await api.zai?.verify(zaiKey);
|
||||
|
||||
if (response?.success && response?.authenticated) {
|
||||
setZaiTestResult({
|
||||
success: true,
|
||||
message: response.message || 'Connection successful! z.ai API responded.',
|
||||
});
|
||||
} else {
|
||||
setZaiTestResult({
|
||||
success: false,
|
||||
message: response?.error || 'Failed to connect to z.ai API.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setZaiTestResult({
|
||||
success: false,
|
||||
message: 'Network error. Please check your connection.',
|
||||
});
|
||||
} finally {
|
||||
setTestingZaiConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Save API keys
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
|
||||
// Configure z.ai service on the server with the new key
|
||||
if (zaiKey && zaiKey.trim().length > 0) {
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.zai.configure(zaiKey.trim());
|
||||
|
||||
if (result.success || result.isAvailable) {
|
||||
// Update z.ai auth status in the store
|
||||
setZaiAuthStatus({
|
||||
authenticated: true,
|
||||
method: 'api_key' as ZaiAuthMethod,
|
||||
hasApiKey: true,
|
||||
hasEnvApiKey: false,
|
||||
});
|
||||
// Invalidate the z.ai usage query so it refetches with the new key
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.usage.zai() });
|
||||
logger.info('z.ai API key configured successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to configure z.ai API key:', error);
|
||||
}
|
||||
} else {
|
||||
// Clear z.ai auth status if key is removed
|
||||
setZaiAuthStatus({
|
||||
authenticated: false,
|
||||
method: 'none' as ZaiAuthMethod,
|
||||
hasApiKey: false,
|
||||
hasEnvApiKey: false,
|
||||
});
|
||||
// Invalidate the query to clear any cached data
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.usage.zai() });
|
||||
}
|
||||
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
@@ -214,6 +303,15 @@ export function useApiKeyManagement() {
|
||||
onTest: handleTestOpenaiConnection,
|
||||
result: openaiTestResult,
|
||||
},
|
||||
zai: {
|
||||
value: zaiKey,
|
||||
setValue: setZaiKey,
|
||||
show: showZaiKey,
|
||||
setShow: setShowZaiKey,
|
||||
testing: testingZaiConnection,
|
||||
onTest: handleTestZaiConnection,
|
||||
result: zaiTestResult,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user