'use client' import * as React from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Bot, Loader2, CheckCircle2, XCircle, ChevronDown, ChevronRight, Server, Wrench, Eye, EyeOff, Database, Link2, Settings, Key, X } from 'lucide-react' import { useT } from '@open-mercato/shared/lib/i18n/context' import { apiCall } from '@open-mercato/ui/backend/utils/apiCall' import { AI_ASSISTANT_LAUNCHER_OPEN_EVENT } from '@open-mercato/ui/ai/AiAssistantLauncher' import { flash } from '@open-mercato/ui/backend/FlashMessages' import { Button } from '@open-mercato/ui/primitives/button' import { Switch } from '@open-mercato/ui/primitives/switch' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select' import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation' import { useAiAssistantVisibility } from '../../../frontend/hooks/useAiAssistantVisibility' import McpConfigDialog from './McpConfigDialog' import SessionKeyDialog from './SessionKeyDialog' type OpenCodeHealthResponse = { status: 'ok' | 'error' opencode?: { healthy: boolean version: string } mcp?: Record search?: { available: boolean driver: string | null url: string | null } url: string mcpUrl: string message?: string } type ProviderConfig = { id: string name: string model: string defaultModel: string envKey: string | null configured: boolean defaultModels: Array<{ id: string; name: string }> } type TenantOverride = { providerId: string | null modelId: string | null baseURL: string | null agentId: string | null updatedAt: string } type SettingsResponse = { provider: ProviderConfig availableProviders: ProviderConfig[] mcpKeyConfigured: boolean resolvedDefault: { providerId: string modelId: string baseURL: string | null source: string } | null tenantOverride: TenantOverride | null agents: AgentResolution[] } type AgentResolution = { agentId: string moduleId: string allowRuntimeOverride: boolean providerId: string modelId: string baseURL: string | null source: string } type ToolInfo = { name: string description: string module: string inputSchema: Record } type AiAssistantSettingsPageClientProps = { launchMode?: 'selector' | 'legacy' showVisibilityControl?: boolean } const LEGACY_AI_ASSISTANT_OPEN_EVENT = 'om:open-ai-chat' async function fetchHealth(): Promise { const result = await apiCall('/api/ai_assistant/health') if (!result.ok || !result.result) throw new Error('Failed to fetch health') return result.result } async function fetchSettings(): Promise { const result = await apiCall('/api/ai_assistant/settings') if (!result.ok || !result.result) throw new Error('Failed to fetch settings') return result.result } async function fetchTools(): Promise<{ tools: ToolInfo[] }> { const result = await apiCall<{ tools: ToolInfo[] }>('/api/ai_assistant/tools') if (!result.ok || !result.result) throw new Error('Failed to fetch tools') return result.result } function GlobalOverrideForm({ availableProviders, tenantOverride, onSaved, }: { availableProviders: ProviderConfig[] tenantOverride: TenantOverride | null onSaved: () => void }) { const t = useT() const [selectedProviderId, setSelectedProviderId] = React.useState( tenantOverride?.providerId ?? '', ) const [selectedModelId, setSelectedModelId] = React.useState( tenantOverride?.modelId ?? '', ) const selectedProvider = availableProviders.find((p) => p.id === selectedProviderId) const { runMutation: runSaveMutation } = useGuardedMutation({ contextId: 'ai-settings-save-override' }) const { runMutation: runClearMutation } = useGuardedMutation({ contextId: 'ai-settings-clear-override' }) const [isSaving, setIsSaving] = React.useState(false) const [isClearing, setIsClearing] = React.useState(false) const handleSave = React.useCallback(async () => { setIsSaving(true) try { await runSaveMutation({ operation: async () => { const result = await apiCall('/api/ai_assistant/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ providerId: selectedProviderId || null, modelId: selectedModelId || null, }), }) if (!result.ok) { const err = (result.result as { error?: string } | null)?.error throw new Error(err ?? t('ai_assistant.settings.saveError', 'Failed to save override.')) } }, context: {}, }) flash(t('ai_assistant.settings.saveSuccess', 'Default model override saved.'), 'success') onSaved() } finally { setIsSaving(false) } }, [onSaved, runSaveMutation, selectedModelId, selectedProviderId, t]) const handleClear = React.useCallback(async () => { setIsClearing(true) try { await runClearMutation({ operation: async () => { const result = await apiCall('/api/ai_assistant/settings', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) if (!result.ok) { const err = (result.result as { error?: string } | null)?.error throw new Error(err ?? t('ai_assistant.settings.clearError', 'Failed to clear override.')) } }, context: {}, }) flash(t('ai_assistant.settings.clearSuccess', 'Default model override cleared.'), 'success') setSelectedProviderId('') setSelectedModelId('') onSaved() } finally { setIsClearing(false) } }, [runClearMutation, onSaved, t]) const isBusy = isSaving || isClearing const configuredProviders = availableProviders.filter((p) => p.configured) return (

{t('ai_assistant.settings.defaultOverrideTitle', 'Default model override')}

{t( 'ai_assistant.settings.defaultOverrideDescription', 'Set a tenant-wide default provider and model. Agents with a per-agent override or specific defaultModel will take precedence.', )}

{tenantOverride && (tenantOverride.providerId || tenantOverride.modelId) ? (
{t('ai_assistant.settings.currentOverride', 'Current override:')} {tenantOverride.providerId ?? '—'} / {tenantOverride.modelId ?? '—'}
) : null}
{t('ai_assistant.settings.providerLabel', 'Provider')}
{t('ai_assistant.settings.modelLabel', 'Model')}
) } function PerAgentOverrideList({ agents, onCleared, }: { agents: AgentResolution[] onCleared: () => void }) { const t = useT() const { runMutation: runClearAgentMutation } = useGuardedMutation({ contextId: 'ai-settings-clear-agent-override' }) const [clearingAgentId, setClearingAgentId] = React.useState(null) const handleClearAgentOverride = React.useCallback( async (agentId: string) => { setClearingAgentId(agentId) try { await runClearAgentMutation({ operation: async () => { const result = await apiCall('/api/ai_assistant/settings', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentId }), }) if (!result.ok) { const err = (result.result as { error?: string } | null)?.error throw new Error(err ?? t('ai_assistant.settings.clearAgentError', 'Failed to clear agent override.')) } }, context: {}, }) flash(t('ai_assistant.settings.clearAgentSuccess', 'Agent override cleared.'), 'success') onCleared() } finally { setClearingAgentId(null) } }, [runClearAgentMutation, onCleared, t], ) if (agents.length === 0) return null const overriddenAgents = agents.filter((agent) => agent.source !== 'env_default' && agent.source !== 'provider_default') return (

{t('ai_assistant.settings.agentOverridesTitle', 'Per-agent model resolution')}

{t( 'ai_assistant.settings.agentOverridesDescription', 'Resolved model for each registered agent. Agents with a custom override show a Clear button.', )}

{agents.map((agent) => { const hasOverride = overriddenAgents.some((a) => a.agentId === agent.agentId) return ( ) })}
{t('ai_assistant.settings.agentIdColumn', 'Agent')} {t('ai_assistant.settings.providerColumn', 'Provider')} {t('ai_assistant.settings.modelColumn', 'Model')} {t('ai_assistant.settings.sourceColumn', 'Source')}
{agent.agentId} {agent.providerId} {agent.modelId} {agent.source} {hasOverride ? ( ) : null}
) } function AiAssistantLauncherCard({ launchMode, showVisibilityControl, }: { launchMode: 'selector' | 'legacy' showVisibilityControl: boolean }) { const t = useT() const { isEnabled, toggleEnabled, isLoaded } = useAiAssistantVisibility() const openAiAssistant = () => { const eventName = launchMode === 'legacy' ? LEGACY_AI_ASSISTANT_OPEN_EVENT : AI_ASSISTANT_LAUNCHER_OPEN_EVENT window.dispatchEvent(new CustomEvent(eventName)) } return (

{t('ai_assistant.settings.visibilityTitle', 'AI Assistant')}

{showVisibilityControl ? isEnabled ? t('ai_assistant.settings.visibilityEnabled', 'Visible in header with Cmd+J shortcut enabled.') : t('ai_assistant.settings.visibilityDisabled', 'Hidden from header. Enable to show the button and Cmd+J shortcut.') : t('ai_assistant.settings.launchDescription', 'Open the AI assistant from this page.')}

{showVisibilityControl ? (
{t('ai_assistant.settings.visibilityToggleLabel', 'Visibility')} {isEnabled ? ( ) : ( )}
) : null}
) } function AiAssistantSettingsContent({ launchMode, showVisibilityControl, }: { launchMode: 'selector' | 'legacy' showVisibilityControl: boolean }) { const t = useT() const queryClient = useQueryClient() const [toolsExpanded, setToolsExpanded] = React.useState(false) const [mcpConfigOpen, setMcpConfigOpen] = React.useState(false) const [sessionKeyOpen, setSessionKeyOpen] = React.useState(false) const healthQuery = useQuery({ queryKey: ['ai-assistant', 'health'], queryFn: fetchHealth, refetchInterval: 10000, staleTime: 5000, }) const settingsQuery = useQuery({ queryKey: ['ai-assistant', 'settings'], queryFn: fetchSettings, staleTime: 60000, }) const toolsQuery = useQuery({ queryKey: ['ai-assistant', 'tools'], queryFn: fetchTools, staleTime: 60000, }) const handleOverrideSaved = React.useCallback(() => { void queryClient.invalidateQueries({ queryKey: ['ai-assistant', 'settings'] }) }, [queryClient]) const isLoading = healthQuery.isLoading || settingsQuery.isLoading || toolsQuery.isLoading if (isLoading) { return (
{t('ai_assistant.settings.loading', 'Loading settings...')}
) } const health = healthQuery.data const settings = settingsQuery.data const tools = toolsQuery.data?.tools ?? [] const toolsByModule = tools.reduce>((acc, tool) => { const module = tool.module || 'other' if (!acc[module]) acc[module] = [] acc[module].push(tool) return acc }, {}) const provider = settings?.provider return (

{t('ai_assistant.settings.pageTitle', 'AI Assistant Settings')}

{t('ai_assistant.settings.pageDescription', 'Configure and monitor the AI assistant')}

{settings ? ( ) : null} {settings?.agents && settings.agents.length > 0 ? ( ) : null}

{t('ai_assistant.settings.connectionsTitle', 'Connections')}

{healthQuery.isFetching && !healthQuery.isLoading ? ( ) : null}

OpenCode

{health?.status === 'ok' && health.opencode?.healthy ? ( {t('ai_assistant.settings.connected', 'Connected')} ) : ( {health?.message ?? t('ai_assistant.settings.disconnected', 'Disconnected')} )}

{health?.opencode?.version ? (

v{health.opencode.version}

) : null}

{health?.url ?? t('ai_assistant.settings.notConfigured', 'Not configured')}

{health?.status === 'ok' && health.opencode?.healthy ? (
) : null}
{(() => { const mcpConnected = health?.mcp && Object.values(health.mcp).some((s) => s.status === 'connected') const mcpConnecting = health?.mcp && Object.values(health.mcp).some((s) => s.status === 'connecting') const mcpError = health?.mcp && Object.values(health.mcp).find((s) => s.error)?.error return (

MCP Server

{mcpConnected ? ( {t('ai_assistant.settings.connected', 'Connected')} ) : mcpConnecting ? ( {t('ai_assistant.settings.connecting', 'Connecting...')} ) : ( {mcpError ?? t('ai_assistant.settings.disconnected', 'Disconnected')} )}

{health?.mcpUrl ?? t('ai_assistant.settings.notConfigured', 'Not configured')}

{mcpConnected ? (
) : null}
) })()}

Meilisearch

{health?.search?.available ? ( {t('ai_assistant.settings.connected', 'Connected')} ) : ( {t('ai_assistant.settings.notAvailable', 'Not available')} )}

{health?.search?.url ?? t('ai_assistant.settings.notConfigured', 'Not configured')}

{health?.search?.available ? (
) : null}
{t('ai_assistant.settings.mcpAuthLabel', 'MCP Authentication:')} {settings?.mcpKeyConfigured ? ( {t('ai_assistant.settings.mcpKeyConfigured', 'MCP_SERVER_API_KEY configured')} ) : ( {t('ai_assistant.settings.mcpKeyMissing', 'MCP_SERVER_API_KEY not set')} )}

{t( 'ai_assistant.settings.mcpAuthNote', 'Required for AI to access platform tools via MCP server.', )}

{t('ai_assistant.settings.llmProviderLabel', 'LLM Provider:')} {provider?.name ?? 'Anthropic'} {provider?.configured ? ( {provider?.envKey ? t('ai_assistant.settings.envKeyConfigured', '{{key}} configured', { key: provider.envKey }) : t('ai_assistant.settings.configured', 'Configured')} ) : ( {t('ai_assistant.settings.envKeyMissing', '{{key}} not set', { key: provider?.envKey ?? 'ANTHROPIC_API_KEY' })} )}

{t( 'ai_assistant.settings.meilisearchNote', 'Meilisearch is required for API endpoint discovery. Endpoints are indexed automatically when the MCP server starts.', )}

{t('ai_assistant.settings.developerToolsTitle', 'Developer Tools')}

{t('ai_assistant.settings.mcpConfigTitle', 'MCP Configuration')}

{t( 'ai_assistant.settings.mcpConfigDescription', 'Generate config for Claude Code or other MCP clients.', )}

{t('ai_assistant.settings.sessionKeyTitle', 'Session API Key')}

{t( 'ai_assistant.settings.sessionKeyDescription', 'Generate a temporary token for programmatic LLM access. Expires after 2 hours.', )}

{toolsExpanded ? (
{Object.entries(toolsByModule).map(([module, moduleTools]) => (

{module}

{moduleTools.map((tool) => (

{tool.name}

{tool.description}

))}
))}
) : null}
) } export function AiAssistantSettingsPageClient({ launchMode = 'selector', showVisibilityControl = false, }: AiAssistantSettingsPageClientProps) { return ( ) } export default AiAssistantSettingsPageClient