@actuate-media/cms-admin 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/views/ai-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/ai-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/ai-settings.render.test.js +182 -0
- package/dist/__tests__/views/ai-settings.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.js +152 -0
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js +16 -9
- package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/MediaPickerModal.d.ts +15 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +17 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +3 -47
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts +6 -0
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -0
- package/dist/views/settings/AISettingsTab.js +138 -0
- package/dist/views/settings/AISettingsTab.js.map +1 -0
- package/dist/views/settings/AiFeaturesCard.d.ts +12 -0
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -0
- package/dist/views/settings/AiFeaturesCard.js +14 -0
- package/dist/views/settings/AiFeaturesCard.js.map +1 -0
- package/dist/views/settings/CapabilityBadges.d.ts +5 -0
- package/dist/views/settings/CapabilityBadges.d.ts.map +1 -0
- package/dist/views/settings/CapabilityBadges.js +26 -0
- package/dist/views/settings/CapabilityBadges.js.map +1 -0
- package/dist/views/settings/ModelSelector.d.ts +12 -0
- package/dist/views/settings/ModelSelector.d.ts.map +1 -0
- package/dist/views/settings/ModelSelector.js +29 -0
- package/dist/views/settings/ModelSelector.js.map +1 -0
- package/dist/views/settings/MonthlyUsageCard.d.ts +14 -0
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -0
- package/dist/views/settings/MonthlyUsageCard.js +30 -0
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -0
- package/dist/views/settings/ProviderConnectionCard.d.ts +13 -0
- package/dist/views/settings/ProviderConnectionCard.d.ts.map +1 -0
- package/dist/views/settings/ProviderConnectionCard.js +48 -0
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts +7 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -0
- package/dist/views/settings/SeoSettingsTab.js +126 -0
- package/dist/views/settings/SeoSettingsTab.js.map +1 -0
- package/dist/views/settings/useAiSettings.d.ts +143 -0
- package/dist/views/settings/useAiSettings.d.ts.map +1 -0
- package/dist/views/settings/useAiSettings.js +278 -0
- package/dist/views/settings/useAiSettings.js.map +1 -0
- package/dist/views/settings/useSeoSettings.d.ts +79 -0
- package/dist/views/settings/useSeoSettings.d.ts.map +1 -0
- package/dist/views/settings/useSeoSettings.js +278 -0
- package/dist/views/settings/useSeoSettings.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/views/ai-settings.render.test.tsx +225 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +219 -0
- package/src/__tests__/views/settings-save-footer.render.test.tsx +16 -9
- package/src/components/MediaPickerModal.tsx +39 -2
- package/src/views/Settings.tsx +4 -141
- package/src/views/settings/AISettingsTab.tsx +411 -0
- package/src/views/settings/AiFeaturesCard.tsx +49 -0
- package/src/views/settings/CapabilityBadges.tsx +66 -0
- package/src/views/settings/ModelSelector.tsx +115 -0
- package/src/views/settings/MonthlyUsageCard.tsx +118 -0
- package/src/views/settings/ProviderConnectionCard.tsx +245 -0
- package/src/views/settings/SeoSettingsTab.tsx +658 -0
- package/src/views/settings/useAiSettings.ts +448 -0
- package/src/views/settings/useSeoSettings.ts +374 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { cmsApi } from '../../lib/api.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Settings → AI state.
|
|
8
|
+
*
|
|
9
|
+
* Two persistence surfaces, mirroring the SEO tab's multi-backend pattern:
|
|
10
|
+
* - Provider connections (create / update / test / disconnect / sync models)
|
|
11
|
+
* act IMMEDIATELY against `/ai/providers/*`. These touch encrypted secrets,
|
|
12
|
+
* so they shouldn't sit in a dirty buffer waiting for a Save click.
|
|
13
|
+
* - Defaults, feature toggles, and the monthly budget are buffered and saved
|
|
14
|
+
* together via `PUT /ai/settings` through the shared SettingsSaveBar.
|
|
15
|
+
*
|
|
16
|
+
* Provider API keys are never returned by the server; we only ever receive a
|
|
17
|
+
* masked `apiKeyLast4` + a source flag.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type AiProviderKey = 'openai' | 'anthropic' | 'xai' | 'custom'
|
|
21
|
+
export type AiConnectionStatus =
|
|
22
|
+
| 'connected'
|
|
23
|
+
| 'invalid_key'
|
|
24
|
+
| 'rate_limited'
|
|
25
|
+
| 'provider_down'
|
|
26
|
+
| 'needs_setup'
|
|
27
|
+
| 'unknown'
|
|
28
|
+
export type AiModelStatus = 'available' | 'unavailable' | 'deprecated' | 'unknown'
|
|
29
|
+
|
|
30
|
+
export interface AiModelView {
|
|
31
|
+
id: string
|
|
32
|
+
provider: AiProviderKey
|
|
33
|
+
providerModelId: string
|
|
34
|
+
displayName: string
|
|
35
|
+
description?: string
|
|
36
|
+
status: AiModelStatus
|
|
37
|
+
capabilities: Record<string, boolean>
|
|
38
|
+
contextWindow?: number
|
|
39
|
+
maxOutputTokens?: number
|
|
40
|
+
inputModalities: string[]
|
|
41
|
+
outputModalities: string[]
|
|
42
|
+
pricing?: {
|
|
43
|
+
inputPerMillion?: number
|
|
44
|
+
outputPerMillion?: number
|
|
45
|
+
image?: number
|
|
46
|
+
search?: number
|
|
47
|
+
}
|
|
48
|
+
lastSyncedAt: string
|
|
49
|
+
source: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AiProviderView {
|
|
53
|
+
id: string
|
|
54
|
+
provider: AiProviderKey
|
|
55
|
+
displayName: string
|
|
56
|
+
enabled: boolean
|
|
57
|
+
status: AiConnectionStatus
|
|
58
|
+
apiKeyLast4?: string
|
|
59
|
+
baseUrl?: string
|
|
60
|
+
organizationId?: string
|
|
61
|
+
projectId?: string
|
|
62
|
+
lastTestedAt?: string
|
|
63
|
+
lastModelSyncAt?: string
|
|
64
|
+
apiKeySource: 'environment' | 'database' | 'config'
|
|
65
|
+
keyManagedExternally: boolean
|
|
66
|
+
hasKey: boolean
|
|
67
|
+
createdAt: string
|
|
68
|
+
updatedAt: string
|
|
69
|
+
updatedBy: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AiFeatureView {
|
|
73
|
+
key: string
|
|
74
|
+
label: string
|
|
75
|
+
description: string
|
|
76
|
+
enabled: boolean
|
|
77
|
+
category: string
|
|
78
|
+
requiredCapabilities: string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AiUsageView {
|
|
82
|
+
totalTokens: number
|
|
83
|
+
promptTokens: number
|
|
84
|
+
completionTokens: number
|
|
85
|
+
estimatedCost?: number
|
|
86
|
+
monthlyTokenLimit?: number
|
|
87
|
+
periodStart: string
|
|
88
|
+
periodEnd: string
|
|
89
|
+
runCount: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The buffered (Save-bar) portion of AI settings. */
|
|
93
|
+
export interface AiSettingsForm {
|
|
94
|
+
defaultProviderId: string
|
|
95
|
+
defaultModelId: string
|
|
96
|
+
features: AiFeatureView[]
|
|
97
|
+
monthlyTokenLimit: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface AiSettingsResponse {
|
|
101
|
+
settings: {
|
|
102
|
+
defaultProviderId?: string
|
|
103
|
+
defaultModelId?: string
|
|
104
|
+
features: AiFeatureView[]
|
|
105
|
+
budget: { monthlyTokenLimit?: number }
|
|
106
|
+
}
|
|
107
|
+
providers: AiProviderView[]
|
|
108
|
+
defaultModel: AiModelView | null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type SaveState = 'idle' | 'saving' | 'success' | 'error'
|
|
112
|
+
|
|
113
|
+
function formFromResponse(res: AiSettingsResponse): AiSettingsForm {
|
|
114
|
+
return {
|
|
115
|
+
defaultProviderId: res.settings.defaultProviderId ?? '',
|
|
116
|
+
defaultModelId: res.settings.defaultModelId ?? '',
|
|
117
|
+
features: res.settings.features.map((f) => ({ ...f })),
|
|
118
|
+
monthlyTokenLimit:
|
|
119
|
+
typeof res.settings.budget.monthlyTokenLimit === 'number'
|
|
120
|
+
? String(res.settings.budget.monthlyTokenLimit)
|
|
121
|
+
: '',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const EMPTY_FORM: AiSettingsForm = {
|
|
126
|
+
defaultProviderId: '',
|
|
127
|
+
defaultModelId: '',
|
|
128
|
+
features: [],
|
|
129
|
+
monthlyTokenLimit: '',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ProviderInput {
|
|
133
|
+
provider: AiProviderKey
|
|
134
|
+
displayName?: string
|
|
135
|
+
apiKey?: string
|
|
136
|
+
baseUrl?: string
|
|
137
|
+
organizationId?: string
|
|
138
|
+
projectId?: string
|
|
139
|
+
enabled?: boolean
|
|
140
|
+
test?: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface UseAiSettings {
|
|
144
|
+
loading: boolean
|
|
145
|
+
loadError: string | null
|
|
146
|
+
providers: AiProviderView[]
|
|
147
|
+
models: AiModelView[]
|
|
148
|
+
usage: AiUsageView | null
|
|
149
|
+
defaultModel: AiModelView | null
|
|
150
|
+
form: AiSettingsForm
|
|
151
|
+
setDefaultProvider: (id: string) => void
|
|
152
|
+
setDefaultModel: (id: string) => void
|
|
153
|
+
setMonthlyTokenLimit: (value: string) => void
|
|
154
|
+
toggleFeature: (key: string, enabled: boolean) => void
|
|
155
|
+
dirty: boolean
|
|
156
|
+
budgetError: string | null
|
|
157
|
+
saveState: SaveState
|
|
158
|
+
saveError: string | null
|
|
159
|
+
save: () => Promise<void>
|
|
160
|
+
reset: () => void
|
|
161
|
+
refetch: () => void
|
|
162
|
+
/** Provider connection actions (persist immediately). */
|
|
163
|
+
createProvider: (input: ProviderInput) => Promise<{ ok: boolean; error?: string }>
|
|
164
|
+
updateProvider: (
|
|
165
|
+
id: string,
|
|
166
|
+
patch: Omit<Partial<ProviderInput>, 'apiKey'> & { apiKey?: string | null },
|
|
167
|
+
) => Promise<{ ok: boolean; error?: string }>
|
|
168
|
+
deleteProvider: (id: string) => Promise<{ ok: boolean; error?: string }>
|
|
169
|
+
testProvider: (
|
|
170
|
+
id: string,
|
|
171
|
+
) => Promise<{ ok: boolean; status?: AiConnectionStatus; message?: string; error?: string }>
|
|
172
|
+
syncModels: (
|
|
173
|
+
id: string,
|
|
174
|
+
) => Promise<{ ok: boolean; fromFallback?: boolean; warning?: string | null; error?: string }>
|
|
175
|
+
busyProviderId: string | null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
179
|
+
const [loading, setLoading] = useState(true)
|
|
180
|
+
const [loadError, setLoadError] = useState<string | null>(null)
|
|
181
|
+
const [providers, setProviders] = useState<AiProviderView[]>([])
|
|
182
|
+
const [models, setModels] = useState<AiModelView[]>([])
|
|
183
|
+
const [usage, setUsage] = useState<AiUsageView | null>(null)
|
|
184
|
+
const [defaultModel, setDefaultModel] = useState<AiModelView | null>(null)
|
|
185
|
+
const [baseline, setBaseline] = useState<AiSettingsForm>(EMPTY_FORM)
|
|
186
|
+
const [form, setForm] = useState<AiSettingsForm>(EMPTY_FORM)
|
|
187
|
+
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
188
|
+
const [saveError, setSaveError] = useState<string | null>(null)
|
|
189
|
+
const [busyProviderId, setBusyProviderId] = useState<string | null>(null)
|
|
190
|
+
const [reloadKey, setReloadKey] = useState(0)
|
|
191
|
+
|
|
192
|
+
const loadModels = useCallback(async () => {
|
|
193
|
+
const res = await cmsApi<{ models: AiModelView[] }>('/ai/models')
|
|
194
|
+
if (!res.error && res.data) setModels(res.data.models ?? [])
|
|
195
|
+
}, [])
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
let cancelled = false
|
|
199
|
+
setLoading(true)
|
|
200
|
+
setLoadError(null)
|
|
201
|
+
Promise.all([
|
|
202
|
+
cmsApi<AiSettingsResponse>('/ai/settings'),
|
|
203
|
+
cmsApi<{ models: AiModelView[] }>('/ai/models'),
|
|
204
|
+
cmsApi<AiUsageView>('/ai/usage/summary'),
|
|
205
|
+
])
|
|
206
|
+
.then(([settingsRes, modelsRes, usageRes]) => {
|
|
207
|
+
if (cancelled) return
|
|
208
|
+
if (settingsRes.error) {
|
|
209
|
+
setLoadError(settingsRes.error)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
if (settingsRes.data) {
|
|
213
|
+
const loaded = formFromResponse(settingsRes.data)
|
|
214
|
+
setBaseline(loaded)
|
|
215
|
+
setForm(loaded)
|
|
216
|
+
setProviders(settingsRes.data.providers ?? [])
|
|
217
|
+
setDefaultModel(settingsRes.data.defaultModel ?? null)
|
|
218
|
+
}
|
|
219
|
+
if (!modelsRes.error && modelsRes.data) setModels(modelsRes.data.models ?? [])
|
|
220
|
+
if (!usageRes.error && usageRes.data) setUsage(usageRes.data)
|
|
221
|
+
})
|
|
222
|
+
.catch(() => {
|
|
223
|
+
if (!cancelled) setLoadError('Failed to load AI settings.')
|
|
224
|
+
})
|
|
225
|
+
.finally(() => {
|
|
226
|
+
if (!cancelled) setLoading(false)
|
|
227
|
+
})
|
|
228
|
+
return () => {
|
|
229
|
+
cancelled = true
|
|
230
|
+
}
|
|
231
|
+
}, [reloadKey])
|
|
232
|
+
|
|
233
|
+
const setDefaultProvider = useCallback((id: string) => {
|
|
234
|
+
setForm((prev) => ({ ...prev, defaultProviderId: id, defaultModelId: '' }))
|
|
235
|
+
setSaveState('idle')
|
|
236
|
+
}, [])
|
|
237
|
+
|
|
238
|
+
const setDefaultModelId = useCallback((id: string) => {
|
|
239
|
+
setForm((prev) => ({ ...prev, defaultModelId: id }))
|
|
240
|
+
setSaveState('idle')
|
|
241
|
+
}, [])
|
|
242
|
+
|
|
243
|
+
const setMonthlyTokenLimit = useCallback((value: string) => {
|
|
244
|
+
setForm((prev) => ({ ...prev, monthlyTokenLimit: value.replace(/[^\d]/g, '') }))
|
|
245
|
+
setSaveState('idle')
|
|
246
|
+
}, [])
|
|
247
|
+
|
|
248
|
+
const toggleFeature = useCallback((key: string, enabled: boolean) => {
|
|
249
|
+
setForm((prev) => ({
|
|
250
|
+
...prev,
|
|
251
|
+
features: prev.features.map((f) => (f.key === key ? { ...f, enabled } : f)),
|
|
252
|
+
}))
|
|
253
|
+
setSaveState('idle')
|
|
254
|
+
}, [])
|
|
255
|
+
|
|
256
|
+
const budgetError = useMemo(() => {
|
|
257
|
+
if (form.monthlyTokenLimit === '') return null
|
|
258
|
+
const n = Number(form.monthlyTokenLimit)
|
|
259
|
+
if (!Number.isFinite(n) || n < 0) return 'Monthly token limit must be a non-negative number.'
|
|
260
|
+
return null
|
|
261
|
+
}, [form.monthlyTokenLimit])
|
|
262
|
+
|
|
263
|
+
const dirty = useMemo(() => {
|
|
264
|
+
if (form.defaultProviderId !== baseline.defaultProviderId) return true
|
|
265
|
+
if (form.defaultModelId !== baseline.defaultModelId) return true
|
|
266
|
+
if (form.monthlyTokenLimit !== baseline.monthlyTokenLimit) return true
|
|
267
|
+
if (form.features.length !== baseline.features.length) return true
|
|
268
|
+
return form.features.some((f, i) => f.enabled !== baseline.features[i]?.enabled)
|
|
269
|
+
}, [form, baseline])
|
|
270
|
+
|
|
271
|
+
const save = useCallback(async () => {
|
|
272
|
+
if (!canEdit || budgetError) return
|
|
273
|
+
setSaveState('saving')
|
|
274
|
+
setSaveError(null)
|
|
275
|
+
try {
|
|
276
|
+
const res = await cmsApi<AiSettingsResponse>('/ai/settings', {
|
|
277
|
+
method: 'PUT',
|
|
278
|
+
body: JSON.stringify({
|
|
279
|
+
defaultProviderId: form.defaultProviderId || null,
|
|
280
|
+
defaultModelId: form.defaultModelId || null,
|
|
281
|
+
features: form.features.map((f) => ({ key: f.key, enabled: f.enabled })),
|
|
282
|
+
budget: {
|
|
283
|
+
monthlyTokenLimit:
|
|
284
|
+
form.monthlyTokenLimit === '' ? null : Number(form.monthlyTokenLimit),
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
})
|
|
288
|
+
if (res.error) throw new Error(res.error)
|
|
289
|
+
setBaseline(form)
|
|
290
|
+
setSaveState('success')
|
|
291
|
+
// Refresh usage so the budget meter reflects the new limit immediately.
|
|
292
|
+
const usageRes = await cmsApi<AiUsageView>('/ai/usage/summary')
|
|
293
|
+
if (!usageRes.error && usageRes.data) setUsage(usageRes.data)
|
|
294
|
+
} catch (err) {
|
|
295
|
+
setSaveState('error')
|
|
296
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save AI settings.')
|
|
297
|
+
}
|
|
298
|
+
}, [canEdit, budgetError, form])
|
|
299
|
+
|
|
300
|
+
const reset = useCallback(() => {
|
|
301
|
+
setForm(baseline)
|
|
302
|
+
setSaveState('idle')
|
|
303
|
+
setSaveError(null)
|
|
304
|
+
}, [baseline])
|
|
305
|
+
|
|
306
|
+
const refetch = useCallback(() => setReloadKey((k) => k + 1), [])
|
|
307
|
+
|
|
308
|
+
const reloadProviders = useCallback(async () => {
|
|
309
|
+
const res = await cmsApi<AiSettingsResponse>('/ai/settings')
|
|
310
|
+
if (!res.error && res.data) {
|
|
311
|
+
setProviders(res.data.providers ?? [])
|
|
312
|
+
setDefaultModel(res.data.defaultModel ?? null)
|
|
313
|
+
}
|
|
314
|
+
}, [])
|
|
315
|
+
|
|
316
|
+
const createProvider = useCallback(
|
|
317
|
+
async (input: ProviderInput) => {
|
|
318
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
319
|
+
setBusyProviderId('new')
|
|
320
|
+
try {
|
|
321
|
+
const res = await cmsApi<AiProviderView>('/ai/providers', {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
body: JSON.stringify(input),
|
|
324
|
+
})
|
|
325
|
+
if (res.error) return { ok: false, error: res.error }
|
|
326
|
+
await reloadProviders()
|
|
327
|
+
return { ok: true }
|
|
328
|
+
} finally {
|
|
329
|
+
setBusyProviderId(null)
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
[canEdit, reloadProviders],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const updateProvider = useCallback(
|
|
336
|
+
async (
|
|
337
|
+
id: string,
|
|
338
|
+
patch: Omit<Partial<ProviderInput>, 'apiKey'> & { apiKey?: string | null },
|
|
339
|
+
) => {
|
|
340
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
341
|
+
setBusyProviderId(id)
|
|
342
|
+
try {
|
|
343
|
+
const res = await cmsApi<AiProviderView>(`/ai/providers/${id}`, {
|
|
344
|
+
method: 'PUT',
|
|
345
|
+
body: JSON.stringify(patch),
|
|
346
|
+
})
|
|
347
|
+
if (res.error) return { ok: false, error: res.error }
|
|
348
|
+
await reloadProviders()
|
|
349
|
+
return { ok: true }
|
|
350
|
+
} finally {
|
|
351
|
+
setBusyProviderId(null)
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
[canEdit, reloadProviders],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const deleteProvider = useCallback(
|
|
358
|
+
async (id: string) => {
|
|
359
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
360
|
+
setBusyProviderId(id)
|
|
361
|
+
try {
|
|
362
|
+
const res = await cmsApi(`/ai/providers/${id}`, { method: 'DELETE' })
|
|
363
|
+
if (res.error) return { ok: false, error: res.error }
|
|
364
|
+
await reloadProviders()
|
|
365
|
+
await loadModels()
|
|
366
|
+
return { ok: true }
|
|
367
|
+
} finally {
|
|
368
|
+
setBusyProviderId(null)
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
[canEdit, reloadProviders, loadModels],
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const testProvider = useCallback(
|
|
375
|
+
async (id: string) => {
|
|
376
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
377
|
+
setBusyProviderId(id)
|
|
378
|
+
try {
|
|
379
|
+
const res = await cmsApi<{
|
|
380
|
+
provider: AiProviderView
|
|
381
|
+
result: { ok: boolean; status: AiConnectionStatus; message: string }
|
|
382
|
+
}>(`/ai/providers/${id}/test`, { method: 'POST' })
|
|
383
|
+
if (res.error) return { ok: false, error: res.error }
|
|
384
|
+
await reloadProviders()
|
|
385
|
+
return {
|
|
386
|
+
ok: res.data?.result.ok ?? false,
|
|
387
|
+
status: res.data?.result.status,
|
|
388
|
+
message: res.data?.result.message,
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
setBusyProviderId(null)
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
[canEdit, reloadProviders],
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const syncModels = useCallback(
|
|
398
|
+
async (id: string) => {
|
|
399
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
400
|
+
setBusyProviderId(id)
|
|
401
|
+
try {
|
|
402
|
+
const res = await cmsApi<{
|
|
403
|
+
models: AiModelView[]
|
|
404
|
+
fromFallback: boolean
|
|
405
|
+
warning: string | null
|
|
406
|
+
}>(`/ai/providers/${id}/sync-models`, { method: 'POST' })
|
|
407
|
+
if (res.error) return { ok: false, error: res.error }
|
|
408
|
+
await loadModels()
|
|
409
|
+
await reloadProviders()
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
fromFallback: res.data?.fromFallback,
|
|
413
|
+
warning: res.data?.warning ?? null,
|
|
414
|
+
}
|
|
415
|
+
} finally {
|
|
416
|
+
setBusyProviderId(null)
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
[canEdit, loadModels, reloadProviders],
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
loading,
|
|
424
|
+
loadError,
|
|
425
|
+
providers,
|
|
426
|
+
models,
|
|
427
|
+
usage,
|
|
428
|
+
defaultModel,
|
|
429
|
+
form,
|
|
430
|
+
setDefaultProvider,
|
|
431
|
+
setDefaultModel: setDefaultModelId,
|
|
432
|
+
setMonthlyTokenLimit,
|
|
433
|
+
toggleFeature,
|
|
434
|
+
dirty,
|
|
435
|
+
budgetError,
|
|
436
|
+
saveState,
|
|
437
|
+
saveError,
|
|
438
|
+
save,
|
|
439
|
+
reset,
|
|
440
|
+
refetch,
|
|
441
|
+
createProvider,
|
|
442
|
+
updateProvider,
|
|
443
|
+
deleteProvider,
|
|
444
|
+
testProvider,
|
|
445
|
+
syncModels,
|
|
446
|
+
busyProviderId,
|
|
447
|
+
}
|
|
448
|
+
}
|