/** * ProviderSelector Component * * Wiederverwendbare Komponente zur Auswahl von AICore-Providern. * Kann im AI Workspace und Automation Editor verwendet werden. * * Selektionsmodell: * ProviderSelection { include: string[], exclude: string[] } * - include(["ALL"]), exclude([]) → alle verfügbaren Provider (dynamisch) * - include(["ALL"]), exclude(["private"]) → alle ausser "private" (dynamisch) * - include(["anthropic"]), exclude([]) → nur Anthropic * - include([]), exclude([]) → keiner ausgewählt * * resolveProviders(selection, allowedProviders) liefert die konkrete Liste. */ import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { useBilling } from '../../hooks/useBilling'; import styles from './ProviderSelector.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; // ============================================================================ // TYPES & HELPERS // ============================================================================ export const PROVIDER_ALL = 'ALL'; export interface ProviderSelection { include: string[]; exclude: string[]; } export function _defaultProviderSelection(): ProviderSelection { return { include: [PROVIDER_ALL], exclude: [] }; } export function _resolveProviders( selection: ProviderSelection, allowedProviders: string[], ): string[] { if (selection.include.includes(PROVIDER_ALL)) { return allowedProviders.filter((p) => !selection.exclude.includes(p)); } return selection.include.filter((p) => allowedProviders.includes(p)); } export function _isAllSelected(selection: ProviderSelection): boolean { return selection.include.includes(PROVIDER_ALL) && selection.exclude.length === 0; } export function _isNoneSelected( selection: ProviderSelection, allowedProviders: string[], ): boolean { return _resolveProviders(selection, allowedProviders).length === 0; } /** * Migrate legacy string[] (old model) to ProviderSelection. * [] → ALL, [...ids] → include those ids. */ export function _migrateFromLegacy(providers: string[]): ProviderSelection { if (providers.length === 0) return _defaultProviderSelection(); return { include: providers, exclude: [] }; } /** * Convert ProviderSelection to flat list for backend API calls. * Returns [] when ALL are selected (= no restriction / legacy behaviour). */ export function _toBackendProviders( selection: ProviderSelection, allowedProviders: string[], ): string[] { if (_isAllSelected(selection)) return []; return _resolveProviders(selection, allowedProviders); } // Provider display names const PROVIDER_LABELS: Record = { anthropic: 'Anthropic (Claude)', openai: 'OpenAI (GPT)', mistral: 'Mistral (Le Chat)', perplexity: 'Perplexity', tavily: 'Tavily (Web Search)', privatellm: 'Private LLM', internal: 'Internal', }; const PROVIDER_ICONS: Record = { anthropic: '🤖', openai: '💬', mistral: '🇫🇷', perplexity: '🔍', tavily: '🌐', privatellm: '🔒', internal: '🏠', }; // ============================================================================ // SINGLE SELECT COMPONENT // ============================================================================ interface ProviderSelectProps { value: string; onChange: (provider: string) => void; disabled?: boolean; className?: string; label?: string; showLabel?: boolean; } export const ProviderSelect: React.FC = ({ value, onChange, disabled = false, className, label = 'AI-Provider', showLabel = true, }) => { const { t } = useLanguage(); const { allowedProviders, loadAllowedProviders, loading } = useBilling(); useEffect(() => { if (allowedProviders.length === 0 && !loading) { loadAllowedProviders(); } }, []); const providerOptions = useMemo(() => { return allowedProviders.map((provider) => ({ value: provider, label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`, })); }, [allowedProviders]); return (
{showLabel && }
); }; // ============================================================================ // MULTI SELECT COMPONENT (Checkbox List) — include / exclude model // ============================================================================ interface ProviderMultiSelectProps { selection: ProviderSelection; onChange: (selection: ProviderSelection) => void; disabled?: boolean; className?: string; label?: string; showLabel?: boolean; defaultExpanded?: boolean; excludeByDefault?: string[]; } export const ProviderMultiSelect: React.FC = ({ selection, onChange, disabled = false, className, label = 'AI-Provider', showLabel = true, defaultExpanded = false, excludeByDefault = [], }) => { const { t } = useLanguage(); const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [initialExcludeApplied, setInitialExcludeApplied] = useState(false); const containerRef = useRef(null); const { allowedProviders, loadAllowedProviders, loading } = useBilling(); useEffect(() => { if (allowedProviders.length === 0 && !loading) { loadAllowedProviders(); } }, []); // Apply default exclusions once when providers first load useEffect(() => { if ( !initialExcludeApplied && allowedProviders.length > 0 && excludeByDefault.length > 0 && _isAllSelected(selection) ) { onChange({ include: [PROVIDER_ALL], exclude: [...excludeByDefault] }); setInitialExcludeApplied(true); } }, [allowedProviders, excludeByDefault, initialExcludeApplied, selection, onChange]); const _handleClickOutside = useCallback((event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsExpanded(false); } }, []); useEffect(() => { if (isExpanded) { document.addEventListener('mousedown', _handleClickOutside); return () => document.removeEventListener('mousedown', _handleClickOutside); } }, [isExpanded, _handleClickOutside]); const effectiveSelection = useMemo( () => _resolveProviders(selection, allowedProviders), [selection, allowedProviders], ); const allSelected = _isAllSelected(selection); const noneSelected = effectiveSelection.length === 0; const _handleToggle = (provider: string) => { const isChecked = effectiveSelection.includes(provider); if (selection.include.includes(PROVIDER_ALL)) { // Currently ALL-based: toggle modifies exclude list if (isChecked) { onChange({ include: [PROVIDER_ALL], exclude: [...selection.exclude, provider] }); } else { const nextExclude = selection.exclude.filter((p) => p !== provider); onChange({ include: [PROVIDER_ALL], exclude: nextExclude }); } } else { // Explicit include list if (isChecked) { onChange({ include: selection.include.filter((p) => p !== provider), exclude: [] }); } else { const nextInclude = [...selection.include, provider]; if (nextInclude.length === allowedProviders.length) { onChange({ include: [PROVIDER_ALL], exclude: [] }); } else { onChange({ include: nextInclude, exclude: [] }); } } } }; const _handleSelectAll = () => { onChange({ include: [PROVIDER_ALL], exclude: [] }); }; const summaryIcon = useMemo(() => { if (noneSelected) return '⊘'; if (effectiveSelection.length === 1) { return PROVIDER_ICONS[effectiveSelection[0]] || '🔌'; } return '⚡'; }, [effectiveSelection, noneSelected]); const summaryHint = useMemo(() => { if (noneSelected) return 'Kein Provider ausgewählt'; if (allSelected) return 'Alle Provider aktiv (dynamisch)'; if (selection.include.includes(PROVIDER_ALL)) { return `Alle ausser ${selection.exclude.length} Provider`; } return `${effectiveSelection.length} von ${allowedProviders.length} Provider`; }, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders]); return (
{isExpanded && (
{showLabel &&
{label}
}
{loading ? (
{t('providerSelector.lade')}
) : (
{allowedProviders.map((provider) => ( ))}
)}
{summaryHint}
)}
); }; // ============================================================================ // COMPACT PROVIDER BADGE LIST // ============================================================================ interface ProviderBadgesProps { providers: string[]; className?: string; } export const ProviderBadges: React.FC = ({ providers, className, }) => { const { t } = useLanguage(); if (providers.length === 0) { return {t('providerSelector.alleProvider')}; } return (
{providers.map((provider) => ( {PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider} ))}
); }; export default ProviderSelect;