@open-mercato/ai-assistant 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +82 -18
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +38 -19
- package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +537 -22
- package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +338 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +123 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +90 -1
- package/dist/modules/ai_assistant/i18n/en.json +90 -1
- package/dist/modules/ai_assistant/i18n/es.json +90 -1
- package/dist/modules/ai_assistant/i18n/pl.json +90 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
- package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
- package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +26 -0
- package/jest.config.cjs +2 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
- package/src/modules/ai_assistant/api/route/route.ts +49 -25
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
- package/src/modules/ai_assistant/api/settings/route.ts +721 -27
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
- package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +164 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
- package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/i18n/de.json +90 -1
- package/src/modules/ai_assistant/i18n/en.json +90 -1
- package/src/modules/ai_assistant/i18n/es.json +90 -1
- package/src/modules/ai_assistant/i18n/pl.json +90 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
- package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
- package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
- package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
- package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
- package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260512130000.ts +13 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { Loader2, Save, Shield, Trash2, Info, AlertCircle, ShieldCheck } from 'lucide-react'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
8
|
+
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
11
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
12
|
+
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
15
|
+
|
|
16
|
+
type EnvAllowlistConfig = {
|
|
17
|
+
providers: string[] | null
|
|
18
|
+
modelsByProvider: Record<string, string[]>
|
|
19
|
+
hasRestrictions: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type TenantAllowlist = {
|
|
23
|
+
allowedProviders: string[] | null
|
|
24
|
+
allowedModelsByProvider: Record<string, string[]>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type EffectiveAllowlist = {
|
|
28
|
+
providers: string[] | null
|
|
29
|
+
modelsByProvider: Record<string, string[]>
|
|
30
|
+
hasRestrictions: boolean
|
|
31
|
+
tenantOverridesActive: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ProviderEntry = {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
defaultModel: string
|
|
38
|
+
envKey: string | null
|
|
39
|
+
configured: boolean
|
|
40
|
+
defaultModels: Array<{ id: string; name: string; contextWindow?: number; tags?: string[] }>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type SettingsResponse = {
|
|
44
|
+
availableProviders: ProviderEntry[]
|
|
45
|
+
allowlistProviders?: ProviderEntry[]
|
|
46
|
+
allowlist: EnvAllowlistConfig
|
|
47
|
+
tenantAllowlist: TenantAllowlist | null
|
|
48
|
+
effectiveAllowlist: EffectiveAllowlist
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchSettings(): Promise<SettingsResponse> {
|
|
52
|
+
const { result, status } = await apiCallOrThrow<SettingsResponse>(
|
|
53
|
+
'/api/ai_assistant/settings',
|
|
54
|
+
{ method: 'GET', credentials: 'include' },
|
|
55
|
+
{ errorMessage: 'Failed to load AI settings' },
|
|
56
|
+
)
|
|
57
|
+
if (!result) throw new Error(`Failed to load settings (${status})`)
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type EditState = {
|
|
62
|
+
/** null = "no tenant restriction (inherit env)"; array = explicit tenant pick */
|
|
63
|
+
allowedProviders: string[] | null
|
|
64
|
+
allowedModelsByProvider: Record<string, string[]>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function snapshotToEditState(snapshot: TenantAllowlist | null): EditState {
|
|
68
|
+
return {
|
|
69
|
+
allowedProviders: snapshot?.allowedProviders ?? null,
|
|
70
|
+
allowedModelsByProvider: { ...(snapshot?.allowedModelsByProvider ?? {}) },
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function AiTenantAllowlistPageClient(): React.JSX.Element {
|
|
75
|
+
const t = useT()
|
|
76
|
+
const queryClient = useQueryClient()
|
|
77
|
+
const settingsQuery = useQuery({ queryKey: ['ai_assistant', 'settings'], queryFn: fetchSettings, staleTime: 0 })
|
|
78
|
+
|
|
79
|
+
const [editState, setEditState] = React.useState<EditState>({
|
|
80
|
+
allowedProviders: null,
|
|
81
|
+
allowedModelsByProvider: {},
|
|
82
|
+
})
|
|
83
|
+
const [dirty, setDirty] = React.useState(false)
|
|
84
|
+
const [saving, setSaving] = React.useState(false)
|
|
85
|
+
const [clearing, setClearing] = React.useState(false)
|
|
86
|
+
const [feedback, setFeedback] = React.useState<{ kind: 'ok' | 'error'; text: string } | null>(null)
|
|
87
|
+
const { runMutation: runSaveAllowlistMutation } = useGuardedMutation({
|
|
88
|
+
contextId: 'ai-tenant-allowlist-save',
|
|
89
|
+
})
|
|
90
|
+
const { runMutation: runClearAllowlistMutation } = useGuardedMutation({
|
|
91
|
+
contextId: 'ai-tenant-allowlist-clear',
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
if (settingsQuery.data) {
|
|
96
|
+
setEditState(snapshotToEditState(settingsQuery.data.tenantAllowlist))
|
|
97
|
+
setDirty(false)
|
|
98
|
+
}
|
|
99
|
+
}, [settingsQuery.data])
|
|
100
|
+
|
|
101
|
+
const pageHeader = (
|
|
102
|
+
<div className="space-y-1">
|
|
103
|
+
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
|
104
|
+
<Shield className="size-6" />
|
|
105
|
+
{t('ai_assistant.allowlist.title', 'AI provider & model allowlist')}
|
|
106
|
+
</h1>
|
|
107
|
+
<p className="text-muted-foreground">
|
|
108
|
+
{t(
|
|
109
|
+
'ai_assistant.allowlist.subtitle',
|
|
110
|
+
'Limit which providers and models the runtime, settings, and chat picker may use for this tenant. The env allowlist is the outer constraint — tenant picks narrow it further.',
|
|
111
|
+
)}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (settingsQuery.isLoading) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex max-w-3xl flex-col gap-4">
|
|
119
|
+
{pageHeader}
|
|
120
|
+
<div className="flex w-fit items-center gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm text-muted-foreground" role="status">
|
|
121
|
+
<Loader2 className="size-4 animate-spin" />
|
|
122
|
+
{t('ai_assistant.allowlist.loading', 'Loading allowlist…')}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (settingsQuery.isError || !settingsQuery.data) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="flex max-w-3xl flex-col gap-4">
|
|
131
|
+
{pageHeader}
|
|
132
|
+
<Alert variant="destructive">
|
|
133
|
+
<AlertCircle className="size-4" />
|
|
134
|
+
<AlertTitle>{t('ai_assistant.allowlist.loadError.title', 'Failed to load allowlist')}</AlertTitle>
|
|
135
|
+
<AlertDescription>
|
|
136
|
+
{settingsQuery.error instanceof Error
|
|
137
|
+
? settingsQuery.error.message
|
|
138
|
+
: t('ai_assistant.allowlist.loadError.body', 'Try refreshing the page.')}
|
|
139
|
+
</AlertDescription>
|
|
140
|
+
</Alert>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const settings = settingsQuery.data
|
|
146
|
+
const envAllowedProviders = settings.allowlist.providers
|
|
147
|
+
const envModelsByProvider = settings.allowlist.modelsByProvider
|
|
148
|
+
|
|
149
|
+
// Provider universe to render: env-allowed providers (or all configured if env unset).
|
|
150
|
+
const editableProviders = settings.allowlistProviders ?? settings.availableProviders
|
|
151
|
+
const candidateProviders = editableProviders.filter((p) => {
|
|
152
|
+
if (envAllowedProviders === null) return true
|
|
153
|
+
return envAllowedProviders.some((id) => id.toLowerCase() === p.id.toLowerCase())
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const tenantPickedProviders = editState.allowedProviders
|
|
157
|
+
const isProviderEnabled = (id: string): boolean => {
|
|
158
|
+
if (tenantPickedProviders === null) return true
|
|
159
|
+
return tenantPickedProviders.includes(id)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const toggleProvider = (id: string, next: boolean): void => {
|
|
163
|
+
setDirty(true)
|
|
164
|
+
setFeedback(null)
|
|
165
|
+
setEditState((prev) => {
|
|
166
|
+
const current = prev.allowedProviders
|
|
167
|
+
if (next) {
|
|
168
|
+
const list = current === null ? [id] : Array.from(new Set([...current, id]))
|
|
169
|
+
return { ...prev, allowedProviders: list }
|
|
170
|
+
}
|
|
171
|
+
const list = current === null
|
|
172
|
+
? candidateProviders.map((p) => p.id).filter((pid) => pid !== id)
|
|
173
|
+
: current.filter((pid) => pid !== id)
|
|
174
|
+
return { ...prev, allowedProviders: list }
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const isModelEnabled = (providerId: string, modelId: string): boolean => {
|
|
179
|
+
const list = editState.allowedModelsByProvider[providerId]
|
|
180
|
+
if (list === undefined) return true
|
|
181
|
+
return list.includes(modelId)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const toggleModel = (providerId: string, modelId: string, next: boolean): void => {
|
|
185
|
+
setDirty(true)
|
|
186
|
+
setFeedback(null)
|
|
187
|
+
const provider = candidateProviders.find((p) => p.id === providerId)
|
|
188
|
+
const allModelIds = provider?.defaultModels.map((m) => m.id) ?? []
|
|
189
|
+
setEditState((prev) => {
|
|
190
|
+
const current = prev.allowedModelsByProvider[providerId]
|
|
191
|
+
const allowedModelsByProvider = { ...prev.allowedModelsByProvider }
|
|
192
|
+
if (next) {
|
|
193
|
+
const list = current === undefined ? [modelId] : Array.from(new Set([...current, modelId]))
|
|
194
|
+
allowedModelsByProvider[providerId] = list
|
|
195
|
+
} else {
|
|
196
|
+
const baseline = current === undefined ? allModelIds : current
|
|
197
|
+
const list = baseline.filter((id) => id !== modelId)
|
|
198
|
+
allowedModelsByProvider[providerId] = list
|
|
199
|
+
}
|
|
200
|
+
return { ...prev, allowedModelsByProvider }
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const resetTenantPicks = (): void => {
|
|
205
|
+
setDirty(true)
|
|
206
|
+
setFeedback(null)
|
|
207
|
+
setEditState({ allowedProviders: null, allowedModelsByProvider: {} })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const handleSave = async (): Promise<void> => {
|
|
211
|
+
setSaving(true)
|
|
212
|
+
setFeedback(null)
|
|
213
|
+
try {
|
|
214
|
+
await runSaveAllowlistMutation({
|
|
215
|
+
operation: async () => {
|
|
216
|
+
const { ok, status, result } = await apiCall<{ error?: string; code?: string }>(
|
|
217
|
+
'/api/ai_assistant/settings/allowlist',
|
|
218
|
+
{
|
|
219
|
+
method: 'PUT',
|
|
220
|
+
credentials: 'include',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
allowedProviders: editState.allowedProviders,
|
|
224
|
+
allowedModelsByProvider: editState.allowedModelsByProvider,
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
if (!ok) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
result?.error ?? t('ai_assistant.allowlist.save.error', `Save failed (${status})`),
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
context: {},
|
|
235
|
+
})
|
|
236
|
+
const successText = t('ai_assistant.allowlist.save.success', 'Allowlist saved.')
|
|
237
|
+
setFeedback({ kind: 'ok', text: successText })
|
|
238
|
+
flash(successText, 'success')
|
|
239
|
+
setDirty(false)
|
|
240
|
+
await queryClient.invalidateQueries({ queryKey: ['ai_assistant', 'settings'] })
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
243
|
+
setFeedback({ kind: 'error', text: message })
|
|
244
|
+
flash(message, 'error')
|
|
245
|
+
} finally {
|
|
246
|
+
setSaving(false)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const handleClear = async (): Promise<void> => {
|
|
251
|
+
setClearing(true)
|
|
252
|
+
setFeedback(null)
|
|
253
|
+
try {
|
|
254
|
+
await runClearAllowlistMutation({
|
|
255
|
+
operation: async () => {
|
|
256
|
+
const { ok, status, result } = await apiCall<{ error?: string; cleared?: boolean }>(
|
|
257
|
+
'/api/ai_assistant/settings/allowlist',
|
|
258
|
+
{ method: 'DELETE', credentials: 'include' },
|
|
259
|
+
)
|
|
260
|
+
if (!ok) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
result?.error ?? t('ai_assistant.allowlist.clear.error', `Clear failed (${status})`),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
context: {},
|
|
267
|
+
})
|
|
268
|
+
const successText = t(
|
|
269
|
+
'ai_assistant.allowlist.clear.success',
|
|
270
|
+
'Tenant allowlist cleared. Env-only enforcement applies.',
|
|
271
|
+
)
|
|
272
|
+
setFeedback({ kind: 'ok', text: successText })
|
|
273
|
+
flash(successText, 'success')
|
|
274
|
+
setDirty(false)
|
|
275
|
+
await queryClient.invalidateQueries({ queryKey: ['ai_assistant', 'settings'] })
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
278
|
+
setFeedback({ kind: 'error', text: message })
|
|
279
|
+
flash(message, 'error')
|
|
280
|
+
} finally {
|
|
281
|
+
setClearing(false)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const envBanner = envAllowedProviders === null && Object.keys(envModelsByProvider).length === 0
|
|
286
|
+
? null
|
|
287
|
+
: (
|
|
288
|
+
<Alert>
|
|
289
|
+
<Info className="h-4 w-4" />
|
|
290
|
+
<AlertTitle>{t('ai_assistant.allowlist.envBanner.title', 'Env allowlist is in effect')}</AlertTitle>
|
|
291
|
+
<AlertDescription className="space-y-1">
|
|
292
|
+
{envAllowedProviders ? (
|
|
293
|
+
<div>
|
|
294
|
+
{t('ai_assistant.allowlist.envBanner.providers', 'OM_AI_AVAILABLE_PROVIDERS')}: <code className="font-mono text-xs">{envAllowedProviders.join(', ')}</code>
|
|
295
|
+
</div>
|
|
296
|
+
) : null}
|
|
297
|
+
{Object.keys(envModelsByProvider).map((pid) => (
|
|
298
|
+
<div key={pid}>
|
|
299
|
+
<code className="font-mono text-xs">OM_AI_AVAILABLE_MODELS_{pid.toUpperCase()}</code>: {envModelsByProvider[pid].join(', ')}
|
|
300
|
+
</div>
|
|
301
|
+
))}
|
|
302
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
303
|
+
{t('ai_assistant.allowlist.envBanner.note', 'Tenant picks may not widen the env list — values outside it are hidden.')}
|
|
304
|
+
</p>
|
|
305
|
+
</AlertDescription>
|
|
306
|
+
</Alert>
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div className="flex flex-col gap-6 max-w-3xl">
|
|
311
|
+
{pageHeader}
|
|
312
|
+
|
|
313
|
+
{envBanner}
|
|
314
|
+
|
|
315
|
+
{feedback ? (
|
|
316
|
+
<Alert variant={feedback.kind === 'error' ? 'destructive' : undefined}>
|
|
317
|
+
{feedback.kind === 'error' ? <AlertCircle className="h-4 w-4" /> : <Info className="h-4 w-4" />}
|
|
318
|
+
<AlertDescription>{feedback.text}</AlertDescription>
|
|
319
|
+
</Alert>
|
|
320
|
+
) : null}
|
|
321
|
+
|
|
322
|
+
<div className="rounded-lg border bg-card p-6 space-y-6">
|
|
323
|
+
<div className="flex items-start justify-between gap-3">
|
|
324
|
+
<div className="space-y-1">
|
|
325
|
+
<h2 className="text-lg font-semibold">{t('ai_assistant.allowlist.providers.title', 'Providers')}</h2>
|
|
326
|
+
<p className="text-sm text-muted-foreground">
|
|
327
|
+
{t(
|
|
328
|
+
'ai_assistant.allowlist.providers.help',
|
|
329
|
+
'Untick to forbid the runtime from using a provider for this tenant. Tick all to inherit the env allowlist.',
|
|
330
|
+
)}
|
|
331
|
+
</p>
|
|
332
|
+
</div>
|
|
333
|
+
<span
|
|
334
|
+
className={
|
|
335
|
+
settings.effectiveAllowlist.tenantOverridesActive
|
|
336
|
+
? 'inline-flex size-8 items-center justify-center rounded-md text-status-success-icon'
|
|
337
|
+
: 'inline-flex size-8 items-center justify-center rounded-md text-status-warning-icon'
|
|
338
|
+
}
|
|
339
|
+
role="img"
|
|
340
|
+
aria-label={
|
|
341
|
+
settings.effectiveAllowlist.tenantOverridesActive
|
|
342
|
+
? t('ai_assistant.allowlist.badge.active', 'Tenant rules active')
|
|
343
|
+
: t('ai_assistant.allowlist.badge.envOnly', 'Env-only')
|
|
344
|
+
}
|
|
345
|
+
title={
|
|
346
|
+
settings.effectiveAllowlist.tenantOverridesActive
|
|
347
|
+
? t('ai_assistant.allowlist.badge.active', 'Tenant rules active')
|
|
348
|
+
: t('ai_assistant.allowlist.badge.envOnly', 'Env-only')
|
|
349
|
+
}
|
|
350
|
+
>
|
|
351
|
+
{settings.effectiveAllowlist.tenantOverridesActive ? (
|
|
352
|
+
<ShieldCheck className="size-5" aria-hidden />
|
|
353
|
+
) : (
|
|
354
|
+
<Shield className="size-5" aria-hidden />
|
|
355
|
+
)}
|
|
356
|
+
</span>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{candidateProviders.length === 0 ? (
|
|
360
|
+
<p className="text-sm text-muted-foreground">
|
|
361
|
+
{t('ai_assistant.allowlist.providers.empty', 'No configured providers within the env allowlist.')}
|
|
362
|
+
</p>
|
|
363
|
+
) : (
|
|
364
|
+
<div className="space-y-4">
|
|
365
|
+
{candidateProviders.map((provider) => {
|
|
366
|
+
const enabled = isProviderEnabled(provider.id)
|
|
367
|
+
const envModels = envModelsByProvider[provider.id]
|
|
368
|
+
const candidateModels = envModels
|
|
369
|
+
? provider.defaultModels.filter((m) => envModels.includes(m.id))
|
|
370
|
+
: provider.defaultModels
|
|
371
|
+
return (
|
|
372
|
+
<div key={provider.id} className="rounded-md border p-4 space-y-3">
|
|
373
|
+
<div className="flex items-center justify-between gap-3">
|
|
374
|
+
<div className="flex items-center gap-3">
|
|
375
|
+
<Checkbox
|
|
376
|
+
id={`provider-${provider.id}`}
|
|
377
|
+
checked={enabled}
|
|
378
|
+
onCheckedChange={(value) => toggleProvider(provider.id, value === true)}
|
|
379
|
+
/>
|
|
380
|
+
<Label htmlFor={`provider-${provider.id}`} className="font-medium">
|
|
381
|
+
{provider.name}
|
|
382
|
+
</Label>
|
|
383
|
+
{provider.configured ? (
|
|
384
|
+
<Badge variant="outline" className="text-xs">
|
|
385
|
+
{t('ai_assistant.allowlist.providers.configured', 'configured')}
|
|
386
|
+
</Badge>
|
|
387
|
+
) : (
|
|
388
|
+
<Badge variant="outline" className="text-xs text-muted-foreground">
|
|
389
|
+
{t('ai_assistant.allowlist.providers.notConfigured', 'not configured')}
|
|
390
|
+
</Badge>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{enabled && candidateModels.length > 0 ? (
|
|
396
|
+
<div className="ml-7 space-y-2">
|
|
397
|
+
<div className="text-xs text-muted-foreground">
|
|
398
|
+
{t('ai_assistant.allowlist.models.help', 'Tick the models tenants may pick. Empty = no model restriction (inherit env).')}
|
|
399
|
+
</div>
|
|
400
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
401
|
+
{candidateModels.map((model) => {
|
|
402
|
+
const checked = isModelEnabled(provider.id, model.id)
|
|
403
|
+
return (
|
|
404
|
+
<label
|
|
405
|
+
key={`${provider.id}-${model.id}`}
|
|
406
|
+
className="flex items-center gap-2 text-sm"
|
|
407
|
+
>
|
|
408
|
+
<Checkbox
|
|
409
|
+
checked={checked}
|
|
410
|
+
onCheckedChange={(value) => toggleModel(provider.id, model.id, value === true)}
|
|
411
|
+
/>
|
|
412
|
+
<span className="font-mono text-xs">{model.id}</span>
|
|
413
|
+
{model.id === provider.defaultModel ? (
|
|
414
|
+
<Badge variant="outline" className="text-xs">
|
|
415
|
+
{t('ai_assistant.allowlist.models.default', 'default')}
|
|
416
|
+
</Badge>
|
|
417
|
+
) : null}
|
|
418
|
+
</label>
|
|
419
|
+
)
|
|
420
|
+
})}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
) : null}
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
})}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
432
|
+
<Button onClick={() => void handleSave()} disabled={!dirty || saving} className="gap-2">
|
|
433
|
+
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
434
|
+
{t('ai_assistant.allowlist.actions.save', 'Save allowlist')}
|
|
435
|
+
</Button>
|
|
436
|
+
<Button
|
|
437
|
+
variant="outline"
|
|
438
|
+
onClick={resetTenantPicks}
|
|
439
|
+
disabled={saving || clearing}
|
|
440
|
+
className="gap-2"
|
|
441
|
+
>
|
|
442
|
+
{t('ai_assistant.allowlist.actions.reset', 'Reset to env defaults')}
|
|
443
|
+
</Button>
|
|
444
|
+
<Button
|
|
445
|
+
variant="ghost"
|
|
446
|
+
onClick={() => void handleClear()}
|
|
447
|
+
disabled={clearing || saving || !settings.tenantAllowlist}
|
|
448
|
+
className="gap-2"
|
|
449
|
+
>
|
|
450
|
+
{clearing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
|
451
|
+
{t('ai_assistant.allowlist.actions.clearStored', 'Clear stored allowlist')}
|
|
452
|
+
</Button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export default AiTenantAllowlistPageClient
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const allowlistIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
+
React.createElement('path', { d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }),
|
|
7
|
+
React.createElement('path', { d: 'm9 12 2 2 4-4' }),
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
requireAuth: true,
|
|
12
|
+
requireFeatures: ['ai_assistant.settings.manage'],
|
|
13
|
+
pageTitle: 'AI Allowlist',
|
|
14
|
+
pageTitleKey: 'ai_assistant.allowlist.navTitle',
|
|
15
|
+
pageGroup: 'Module Configs',
|
|
16
|
+
pageGroupKey: 'settings.sections.moduleConfigs',
|
|
17
|
+
pageOrder: 432,
|
|
18
|
+
icon: allowlistIcon,
|
|
19
|
+
pageContext: 'settings' as const,
|
|
20
|
+
breadcrumb: [
|
|
21
|
+
{ label: 'AI Allowlist', labelKey: 'ai_assistant.allowlist.navTitle' },
|
|
22
|
+
],
|
|
23
|
+
} as const
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
|
+
import { AiTenantAllowlistPageClient } from './AiTenantAllowlistPageClient'
|
|
3
|
+
|
|
4
|
+
export default async function AiAssistantAllowlistPage() {
|
|
5
|
+
return (
|
|
6
|
+
<Page>
|
|
7
|
+
<PageBody>
|
|
8
|
+
<AiTenantAllowlistPageClient />
|
|
9
|
+
</PageBody>
|
|
10
|
+
</Page>
|
|
11
|
+
)
|
|
12
|
+
}
|
package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx
CHANGED
|
@@ -110,7 +110,10 @@ function PlaygroundNoAgents() {
|
|
|
110
110
|
function AgentDetails({ agent }: { agent: PlaygroundAgent }) {
|
|
111
111
|
const t = useT()
|
|
112
112
|
return (
|
|
113
|
-
<div
|
|
113
|
+
<div
|
|
114
|
+
className="rounded-md border border-border bg-muted/30 p-3 text-sm"
|
|
115
|
+
data-ai-playground-agent={agent.id}
|
|
116
|
+
>
|
|
114
117
|
<div className="font-semibold">{agent.label}</div>
|
|
115
118
|
<p className="mt-1 text-xs text-muted-foreground">{agent.description}</p>
|
|
116
119
|
<dl className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
|
@@ -175,6 +178,77 @@ function buildDebugPromptSections(agent: PlaygroundAgent): AiChatDebugPromptSect
|
|
|
175
178
|
return sections
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
type AgentModelResolution = {
|
|
182
|
+
agentId: string
|
|
183
|
+
providerId: string
|
|
184
|
+
modelId: string
|
|
185
|
+
baseURL: string | null
|
|
186
|
+
source: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type SettingsAgentResolutionResponse = {
|
|
190
|
+
agents: AgentModelResolution[]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function fetchAgentResolutions(): Promise<SettingsAgentResolutionResponse> {
|
|
194
|
+
const result = await apiCall<SettingsAgentResolutionResponse>('/api/ai_assistant/settings')
|
|
195
|
+
if (!result.ok || !result.result) return { agents: [] }
|
|
196
|
+
return { agents: result.result.agents ?? [] }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ModelResolutionPanel({ agentId }: { agentId: string }) {
|
|
200
|
+
const t = useT()
|
|
201
|
+
const { data } = useQuery({
|
|
202
|
+
queryKey: ['ai_assistant', 'settings', 'agents'],
|
|
203
|
+
queryFn: fetchAgentResolutions,
|
|
204
|
+
staleTime: 30000,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const resolution = data?.agents.find((agent) => agent.agentId === agentId)
|
|
208
|
+
if (!resolution) return null
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<dl
|
|
212
|
+
className="grid grid-cols-2 gap-x-4 gap-y-1 rounded-md border border-border bg-muted/30 p-3 text-xs sm:grid-cols-4"
|
|
213
|
+
data-ai-playground-model-resolution={agentId}
|
|
214
|
+
data-ai-playground-resolution-panel={agentId}
|
|
215
|
+
>
|
|
216
|
+
<div>
|
|
217
|
+
<dt className="font-medium text-muted-foreground">
|
|
218
|
+
{t('ai_assistant.playground.resolution.provider', 'Provider')}
|
|
219
|
+
</dt>
|
|
220
|
+
<dd className="font-mono" data-ai-playground-resolution-provider>
|
|
221
|
+
{resolution.providerId}
|
|
222
|
+
</dd>
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<dt className="font-medium text-muted-foreground">
|
|
226
|
+
{t('ai_assistant.playground.resolution.model', 'Model')}
|
|
227
|
+
</dt>
|
|
228
|
+
<dd className="font-mono" data-ai-playground-resolution-model>
|
|
229
|
+
{resolution.modelId}
|
|
230
|
+
</dd>
|
|
231
|
+
</div>
|
|
232
|
+
<div>
|
|
233
|
+
<dt className="font-medium text-muted-foreground">
|
|
234
|
+
{t('ai_assistant.playground.resolution.baseUrl', 'Base URL')}
|
|
235
|
+
</dt>
|
|
236
|
+
<dd className="font-mono" data-ai-playground-resolution-base-url>
|
|
237
|
+
{resolution.baseURL ?? t('ai_assistant.playground.resolution.none', '—')}
|
|
238
|
+
</dd>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<dt className="font-medium text-muted-foreground">
|
|
242
|
+
{t('ai_assistant.playground.resolution.source', 'Source')}
|
|
243
|
+
</dt>
|
|
244
|
+
<dd className="font-mono" data-ai-playground-resolution-source>
|
|
245
|
+
{resolution.source}
|
|
246
|
+
</dd>
|
|
247
|
+
</div>
|
|
248
|
+
</dl>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
178
252
|
type PlaygroundUiPartSeed = {
|
|
179
253
|
componentId: string
|
|
180
254
|
pendingActionId?: string
|
|
@@ -243,17 +317,19 @@ function ChatLane({ agent, debug }: { agent: PlaygroundAgent; debug: boolean })
|
|
|
243
317
|
}
|
|
244
318
|
|
|
245
319
|
return (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
<div data-ai-playground-chat={agent.id}>
|
|
321
|
+
<AiChat
|
|
322
|
+
key={agent.id}
|
|
323
|
+
agent={agent.id}
|
|
324
|
+
pageContext={{ source: 'playground', pageId: 'ai_assistant.playground' }}
|
|
325
|
+
debug={debug}
|
|
326
|
+
registry={registry}
|
|
327
|
+
className="min-h-96"
|
|
328
|
+
debugTools={debugTools}
|
|
329
|
+
debugPromptSections={debugPromptSections}
|
|
330
|
+
uiParts={uiParts}
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
257
333
|
)
|
|
258
334
|
}
|
|
259
335
|
|
|
@@ -574,6 +650,7 @@ export function AiPlaygroundPageClient() {
|
|
|
574
650
|
</div>
|
|
575
651
|
</div>
|
|
576
652
|
{selectedAgent ? <AgentDetails agent={selectedAgent} /> : null}
|
|
653
|
+
{selectedAgent ? <ModelResolutionPanel agentId={selectedAgent.id} /> : null}
|
|
577
654
|
</section>
|
|
578
655
|
|
|
579
656
|
{selectedAgent ? (
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const settingsIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
+
React.createElement('circle', { cx: 12, cy: 12, r: 3 }),
|
|
7
|
+
React.createElement('path', { d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z' }),
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
requireAuth: true,
|
|
12
|
+
requireFeatures: ['ai_assistant.settings.manage'],
|
|
13
|
+
pageTitle: 'AI Settings',
|
|
14
|
+
pageTitleKey: 'ai_assistant.config.nav.settings',
|
|
15
|
+
pageGroup: 'Module Configs',
|
|
16
|
+
pageGroupKey: 'settings.sections.moduleConfigs',
|
|
17
|
+
pageOrder: 430,
|
|
18
|
+
icon: settingsIcon,
|
|
19
|
+
pageContext: 'settings' as const,
|
|
20
|
+
breadcrumb: [
|
|
21
|
+
{ label: 'AI Settings', labelKey: 'ai_assistant.config.nav.settings' },
|
|
22
|
+
],
|
|
23
|
+
} as const
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
|
+
import { AiAssistantSettingsPageClient } from '../../../../components/AiAssistantSettingsPageClient'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical AI assistant settings route. Renders the Phase 4b override
|
|
6
|
+
* form + per-agent resolution table. Phase 1780-6 introduced the dedicated
|
|
7
|
+
* `/allowlist` page on top of this one; both routes share the same
|
|
8
|
+
* `ai_assistant.settings.manage` feature gate.
|
|
9
|
+
*/
|
|
10
|
+
export default async function AiAssistantSettingsPage() {
|
|
11
|
+
return (
|
|
12
|
+
<Page>
|
|
13
|
+
<PageBody>
|
|
14
|
+
<AiAssistantSettingsPageClient />
|
|
15
|
+
</PageBody>
|
|
16
|
+
</Page>
|
|
17
|
+
)
|
|
18
|
+
}
|