From 9c78cf7f846f73fa0255c756b3c4abcf16e81055 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 12 Dec 2025 12:23:06 +0000 Subject: [PATCH] Move AI cost budget setting into AI settings --- .../src/components/AI/AICostDashboard.tsx | 62 +++---------------- .../src/components/Settings/AISettings.tsx | 53 ++++++++++++++++ 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/frontend-modern/src/components/AI/AICostDashboard.tsx b/frontend-modern/src/components/AI/AICostDashboard.tsx index 0b6ba96..6620c1d 100644 --- a/frontend-modern/src/components/AI/AICostDashboard.tsx +++ b/frontend-modern/src/components/AI/AICostDashboard.tsx @@ -57,8 +57,6 @@ export const AICostDashboard: Component = () => { const [loadError, setLoadError] = createSignal(null); const [summary, setSummary] = createSignal(null); const [aiSettings, setAISettings] = createSignal(null); - const [budgetUSD30dInput, setBudgetUSD30dInput] = createSignal(''); - const [savingBudget, setSavingBudget] = createSignal(false); let requestSeq = 0; const anyPricingKnown = createMemo(() => { @@ -124,15 +122,9 @@ export const AICostDashboard: Component = () => { try { const s = await AIAPI.getSettings(); setAISettings(s); - if (typeof s.cost_budget_usd_30d === 'number' && Number.isFinite(s.cost_budget_usd_30d) && s.cost_budget_usd_30d > 0) { - setBudgetUSD30dInput(String(s.cost_budget_usd_30d)); - } else { - setBudgetUSD30dInput(''); - } } catch (err) { logger.debug('[AICostDashboard] Failed to load AI settings for budget:', err); setAISettings(null); - setBudgetUSD30dInput(''); } }; @@ -141,10 +133,9 @@ export const AICostDashboard: Component = () => { }); const parsedBudgetUSD30d = createMemo(() => { - const raw = budgetUSD30dInput().trim(); - if (!raw) return null; - const n = Number(raw); - if (!Number.isFinite(n) || n <= 0) return null; + const s = aiSettings(); + const n = s?.cost_budget_usd_30d; + if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) return null; return n; }); @@ -162,30 +153,6 @@ export const AICostDashboard: Component = () => { return usd > budget; }); - const isDirtyBudget = createMemo(() => { - const s = aiSettings(); - const current = typeof s?.cost_budget_usd_30d === 'number' ? s.cost_budget_usd_30d : 0; - const next = parsedBudgetUSD30d() ?? 0; - return Math.abs(current - next) > 0.0001; - }); - - const saveBudget = async () => { - if (savingBudget()) return; - setSavingBudget(true); - try { - const value = parsedBudgetUSD30d() ?? 0; - const next = await AIAPI.updateSettings({ cost_budget_usd_30d: value }); - setAISettings(next); - notificationStore.success('AI cost budget saved'); - } catch (err) { - logger.error('[AICostDashboard] Failed to save budget:', err); - notificationStore.error('Failed to save AI cost budget'); - await loadBudgetSettings(); - } finally { - setSavingBudget(false); - } - }; - const resetHistory = async () => { if (!confirm('Reset AI usage history? A backup will be created in the Pulse config directory.')) return; try { @@ -386,25 +353,14 @@ export const AICostDashboard: Component = () => {
-
-
Budget alert (USD per 30d)
- +
Budget alert (USD per 30d)
+
+ —}> + {formatUSD(parsedBudgetUSD30d() ?? 0)} +
- setBudgetUSD30dInput(e.currentTarget.value)} - placeholder="e.g. 25" - class="mt-1 w-full px-2 py-1 text-sm rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" - />
- Cross-provider estimate. Pro-rated for {days()}d:{' '} + Set in AI settings. Pro-rated for {days()}d:{' '} —}> {formatUSD(budgetForRange() ?? 0)} diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index 0447d60..ebe7bdb 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -92,6 +92,8 @@ export const AISettings: Component = () => { deepseekApiKey: '', ollamaBaseUrl: 'http://localhost:11434', openaiBaseUrl: '', + // Cost controls + costBudgetUSD30d: '', }); const resetForm = (data: AISettingsType | null) => { @@ -116,6 +118,7 @@ export const AISettings: Component = () => { deepseekApiKey: '', ollamaBaseUrl: 'http://localhost:11434', openaiBaseUrl: '', + costBudgetUSD30d: '', }); return; } @@ -140,6 +143,10 @@ export const AISettings: Component = () => { deepseekApiKey: '', ollamaBaseUrl: data.ollama_base_url || 'http://localhost:11434', openaiBaseUrl: data.openai_base_url || '', + costBudgetUSD30d: + typeof data.cost_budget_usd_30d === 'number' && data.cost_budget_usd_30d > 0 + ? String(data.cost_budget_usd_30d) + : '', }); // Auto-expand providers that are configured @@ -291,6 +298,20 @@ export const AISettings: Component = () => { payload.openai_base_url = form.openaiBaseUrl.trim(); } + // Cost controls (server-side budget, cross-provider estimate) + { + const raw = form.costBudgetUSD30d.trim(); + const parsed = raw === '' ? 0 : Number(raw); + if (!Number.isFinite(parsed) || parsed < 0) { + notificationStore.error('Cost budget must be a non-negative number'); + return; + } + const current = settings()?.cost_budget_usd_30d ?? 0; + if (Math.abs(parsed - current) > 0.0001) { + payload.cost_budget_usd_30d = parsed; + } + } + const updated = await AIAPI.updateSettings(payload); setSettings(updated); resetForm(updated); @@ -1054,6 +1075,38 @@ export const AISettings: Component = () => {
+ {/* AI Cost Controls */} +
+
+ +

+ This budget is a cross-provider estimate for Pulse usage. Provider dashboards remain the source of truth for billing. +

+
+ +
+ + setForm('costBudgetUSD30d', e.currentTarget.value)} + min={0} + step={1} + placeholder="0 (disabled)" + disabled={saving()} + style={{ width: '180px' }} + /> +

+ Set to 0 to disable. Cost dashboard pro-rates for shorter ranges. +

+
+
+