304 lines
9.3 KiB
TypeScript
304 lines
9.3 KiB
TypeScript
/**
|
|
* ProviderSelector Component
|
|
*
|
|
* Wiederverwendbare Komponente zur Auswahl von AICore-Providern.
|
|
* Kann im Chat Playground und Automation Editor verwendet werden.
|
|
*
|
|
* Features:
|
|
* - Dropdown für Einzelauswahl
|
|
* - Checkbox-Liste für Mehrfachauswahl
|
|
* - Lädt verfügbare Provider aus dem Billing-System
|
|
*/
|
|
|
|
import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react';
|
|
import { useBilling } from '../../hooks/useBilling';
|
|
import styles from './ProviderSelector.module.css';
|
|
|
|
// Provider display names
|
|
const PROVIDER_LABELS: Record<string, string> = {
|
|
anthropic: 'Anthropic (Claude)',
|
|
openai: 'OpenAI (GPT)',
|
|
mistral: 'Mistral (Le Chat)',
|
|
perplexity: 'Perplexity',
|
|
tavily: 'Tavily (Web Search)',
|
|
privatellm: 'Private LLM',
|
|
internal: 'Internal',
|
|
};
|
|
|
|
// Provider icons (emojis for simplicity)
|
|
const PROVIDER_ICONS: Record<string, string> = {
|
|
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<ProviderSelectProps> = ({
|
|
value,
|
|
onChange,
|
|
disabled = false,
|
|
className,
|
|
label = 'AI-Provider',
|
|
showLabel = true,
|
|
}) => {
|
|
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 (
|
|
<div className={`${styles.providerSelect} ${className || ''}`}>
|
|
{showLabel && <label className={styles.label}>{label}</label>}
|
|
<select
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled || loading}
|
|
className={styles.select}
|
|
>
|
|
<option value="">-- Auto --</option>
|
|
{providerOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// MULTI SELECT COMPONENT (Checkbox List)
|
|
// ============================================================================
|
|
|
|
interface ProviderMultiSelectProps {
|
|
selectedProviders: string[];
|
|
onChange: (providers: string[]) => void;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
label?: string;
|
|
showLabel?: boolean;
|
|
defaultExpanded?: boolean;
|
|
excludeByDefault?: string[];
|
|
}
|
|
|
|
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|
selectedProviders,
|
|
onChange,
|
|
disabled = false,
|
|
className,
|
|
label = 'AI-Provider',
|
|
showLabel = true,
|
|
defaultExpanded = false,
|
|
excludeByDefault = [],
|
|
}) => {
|
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
|
|
|
useEffect(() => {
|
|
if (allowedProviders.length === 0 && !loading) {
|
|
loadAllowedProviders();
|
|
}
|
|
}, []);
|
|
|
|
// Apply default exclusions when providers first load
|
|
useEffect(() => {
|
|
if (
|
|
!initialExcludeApplied &&
|
|
allowedProviders.length > 0 &&
|
|
excludeByDefault.length > 0 &&
|
|
selectedProviders.length === 0
|
|
) {
|
|
const initialSelection = allowedProviders.filter(
|
|
(p) => !excludeByDefault.includes(p)
|
|
);
|
|
// Only apply if there's actually something to exclude
|
|
if (initialSelection.length < allowedProviders.length) {
|
|
onChange(initialSelection);
|
|
}
|
|
setInitialExcludeApplied(true);
|
|
}
|
|
}, [allowedProviders, excludeByDefault, initialExcludeApplied, selectedProviders.length, onChange]);
|
|
|
|
// Click outside handler
|
|
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]);
|
|
|
|
// Effective selection: empty array = all providers active (no restriction)
|
|
const effectiveSelection = selectedProviders.length === 0 ? allowedProviders : selectedProviders;
|
|
|
|
// "Alle" is active when no restriction is set (empty array) OR all explicitly selected
|
|
const isAllSelected = selectedProviders.length === 0 ||
|
|
(allowedProviders.length > 0 && selectedProviders.length === allowedProviders.length);
|
|
|
|
const handleToggle = (provider: string) => {
|
|
if (selectedProviders.length === 0) {
|
|
// Currently "all active" (no restriction) -> make explicit: all except the toggled one
|
|
onChange(allowedProviders.filter((p) => p !== provider));
|
|
} else if (selectedProviders.includes(provider)) {
|
|
// Deactivate: remove from selection
|
|
const remaining = selectedProviders.filter((p) => p !== provider);
|
|
// If removing leaves all others selected, reset to [] (= all, no restriction)
|
|
if (remaining.length === allowedProviders.length) {
|
|
onChange([]);
|
|
} else {
|
|
onChange(remaining);
|
|
}
|
|
} else {
|
|
// Activate: add to selection
|
|
const updated = [...selectedProviders, provider];
|
|
// If all are now selected, reset to [] (= all, no restriction)
|
|
if (updated.length === allowedProviders.length) {
|
|
onChange([]);
|
|
} else {
|
|
onChange(updated);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
onChange([]); // Empty = all active, no restriction
|
|
};
|
|
|
|
// Summary icon for button
|
|
const summaryIcon = useMemo(() => {
|
|
if (effectiveSelection.length === 1) {
|
|
return PROVIDER_ICONS[effectiveSelection[0]] || '🔌';
|
|
}
|
|
return '🤖';
|
|
}, [effectiveSelection]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`${styles.providerMultiSelect} ${className || ''} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
|
>
|
|
{/* Trigger Button - styled like iconButton */}
|
|
<button
|
|
type="button"
|
|
className={styles.triggerButton}
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
disabled={disabled}
|
|
title="Provider auswählen"
|
|
>
|
|
<span className={styles.buttonIcon}>{summaryIcon}</span>
|
|
</button>
|
|
|
|
{/* Dropdown Content */}
|
|
{isExpanded && (
|
|
<div className={styles.dropdownContent}>
|
|
{showLabel && <div className={styles.dropdownHeader}>{label}</div>}
|
|
|
|
<div className={styles.selectActions}>
|
|
<button
|
|
type="button"
|
|
onClick={handleSelectAll}
|
|
disabled={disabled}
|
|
className={`${styles.actionButton} ${isAllSelected ? styles.active : ''}`}
|
|
>
|
|
Alle
|
|
</button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className={styles.loading}>Lade...</div>
|
|
) : (
|
|
<div className={styles.checkboxList}>
|
|
{allowedProviders.map((provider) => (
|
|
<label
|
|
key={provider}
|
|
className={`${styles.checkboxItem} ${disabled ? styles.disabled : ''}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={effectiveSelection.includes(provider)}
|
|
onChange={() => handleToggle(provider)}
|
|
disabled={disabled}
|
|
/>
|
|
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
|
<span className={styles.providerName}>
|
|
{PROVIDER_LABELS[provider] || provider}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{isAllSelected && !loading && (
|
|
<div className={styles.hint}>
|
|
Alle Provider aktiv (kein Filter)
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// COMPACT PROVIDER BADGE LIST
|
|
// ============================================================================
|
|
|
|
interface ProviderBadgesProps {
|
|
providers: string[];
|
|
className?: string;
|
|
}
|
|
|
|
export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
|
providers,
|
|
className,
|
|
}) => {
|
|
if (providers.length === 0) {
|
|
return <span className={styles.allProviders}>Alle Provider</span>;
|
|
}
|
|
|
|
return (
|
|
<div className={`${styles.providerBadges} ${className || ''}`}>
|
|
{providers.map((provider) => (
|
|
<span key={provider} className={styles.badge}>
|
|
{PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider}
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Default export
|
|
export default ProviderSelect;
|