715 lines
28 KiB
TypeScript
715 lines
28 KiB
TypeScript
/**
|
||
* AdminFeatureAccessPage
|
||
*
|
||
* Admin page for managing feature instances within mandates.
|
||
* Allows creating, viewing, and managing feature instances.
|
||
*/
|
||
|
||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||
import { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess';
|
||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||
import { useFeatureStore } from '../../stores/featureStore';
|
||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||
import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/fa';
|
||
import { useToast } from '../../contexts/ToastContext';
|
||
import api from '../../api';
|
||
import { useApiRequest } from '../../hooks/useApi';
|
||
import { fetchAttributes } from '../../api/attributesApi';
|
||
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
|
||
import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
||
import { ChatbotConfigSection } from './ChatbotConfigSection';
|
||
import { TextField } from '../../components/UiComponents/TextField';
|
||
import styles from './Admin.module.css';
|
||
|
||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||
import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils';
|
||
|
||
export const AdminFeatureAccessPage: React.FC = () => {
|
||
const { t } = useLanguage();
|
||
|
||
const {
|
||
features,
|
||
instances,
|
||
instancesPagination,
|
||
loading,
|
||
error,
|
||
fetchFeatures,
|
||
fetchInstances,
|
||
createInstance,
|
||
updateInstance,
|
||
deleteInstance,
|
||
syncInstanceRoles,
|
||
syncInstanceWorkflows,
|
||
} = useFeatureAccess();
|
||
|
||
const { fetchMandates } = useUserMandates();
|
||
const { showSuccess, showError } = useToast();
|
||
const { loadFeatures } = useFeatureStore();
|
||
const { request } = useApiRequest();
|
||
|
||
// State
|
||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [showEditModal, setShowEditModal] = useState(false);
|
||
const [editingInstance, setEditingInstance] = useState<FeatureInstance | null>(null);
|
||
const [, setIsSubmitting] = useState(false);
|
||
const [syncingInstance, setSyncingInstance] = useState<string | null>(null);
|
||
const [syncingWorkflowsInstance, setSyncingWorkflowsInstance] = useState<string | null>(null);
|
||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||
|
||
// Chatbot configuration state
|
||
const [createFeatureCode, setCreateFeatureCode] = useState<string>('');
|
||
const [createLabel, setCreateLabel] = useState<string>(''); // Label field value
|
||
const [chatbotConnectors, setChatbotConnectors] = useState<string[]>(['preprocessor']); // Array for multiselect (database connectors only)
|
||
const [chatbotSystemPrompt, setChatbotSystemPrompt] = useState<string>('');
|
||
const [chatbotEnableWebResearch, setChatbotEnableWebResearch] = useState<boolean>(true); // Enable Tavily web research
|
||
const [chatbotAllowedProviders, setChatbotAllowedProviders] = useState<string[]>([]); // Allowed LLM providers (empty = all)
|
||
|
||
// Ref to track form data for featureCode detection
|
||
const formDataRef = useRef<Record<string, any>>({});
|
||
|
||
// Load features, mandates, and attributes on mount
|
||
useEffect(() => {
|
||
fetchFeatures();
|
||
fetchMandates().then(data => {
|
||
setMandates(data);
|
||
if (data.length > 0 && !selectedMandateId) {
|
||
setSelectedMandateId(data[0].id);
|
||
}
|
||
});
|
||
// Fetch FeatureInstance attributes from backend
|
||
api.get('/api/attributes/FeatureInstance').then(response => {
|
||
const attrs = response.data?.attributes || response.data || [];
|
||
setBackendAttributes(Array.isArray(attrs) ? attrs : []);
|
||
}).catch(() => setBackendAttributes([]));
|
||
}, [fetchFeatures, fetchMandates]);
|
||
|
||
// Load instances when mandate changes
|
||
useEffect(() => {
|
||
if (selectedMandateId) {
|
||
fetchInstances(selectedMandateId);
|
||
}
|
||
}, [selectedMandateId, fetchInstances]);
|
||
|
||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||
{ key: 'label', label: t('Name'), sortable: true, filterable: true, searchable: true, width: 200 },
|
||
{
|
||
key: 'featureCode',
|
||
label: t('Feature'),
|
||
sortable: true,
|
||
filterable: true,
|
||
width: 150,
|
||
formatter: (value: string) => {
|
||
const feature = features.find(f => f.code === value);
|
||
const label = feature ? (feature.label || value) : value;
|
||
return label;
|
||
},
|
||
},
|
||
{ key: 'enabled', label: t('Aktiv'), sortable: true, filterable: true, width: 80 },
|
||
], [features, t]);
|
||
|
||
const columns = useMemo(
|
||
() => resolveColumnTypes(_rawColumns, backendAttributes),
|
||
[_rawColumns, backendAttributes],
|
||
);
|
||
|
||
// Form attributes from backend - merge with dynamic feature options
|
||
// Exclude featureCode, config, and label since we handle them separately
|
||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||
const excludedFields = ['id', 'mandateId', 'featureCode', 'config', 'label']; // Exclude featureCode, config, and label - handled separately
|
||
|
||
return backendAttributes
|
||
.filter(attr => !excludedFields.includes(attr.name))
|
||
.map(attr => ({
|
||
...attr,
|
||
editable: attr.name === 'enabled' ? true : attr.editable,
|
||
})) as AttributeDefinition[];
|
||
}, [backendAttributes]);
|
||
|
||
// Handle create instance
|
||
const handleCreateInstance = async (data: { featureCode: string; enabled?: boolean; copyTemplateRoles?: boolean }) => {
|
||
if (!selectedMandateId) return;
|
||
setIsSubmitting(true);
|
||
try {
|
||
// Validate label
|
||
if (!createLabel || createLabel.trim() === '') {
|
||
showError(t('Fehler'), t('Label ist erforderlich.'));
|
||
setIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
// Build config for chatbot instances
|
||
let config: Record<string, any> | undefined = undefined;
|
||
if (createFeatureCode === 'chatbot') {
|
||
// Validate required fields
|
||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||
setIsSubmitting(false);
|
||
return;
|
||
}
|
||
const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null;
|
||
config = {
|
||
connector: chatbotConnectors.length > 0 ? {
|
||
types: chatbotConnectors,
|
||
type: primaryConnector,
|
||
customConnectorClass: null
|
||
} : undefined,
|
||
prompts: {
|
||
useCustomPrompts: true,
|
||
customAnalysisPrompt: chatbotSystemPrompt,
|
||
customFinalAnswerPrompt: chatbotSystemPrompt
|
||
},
|
||
behavior: {
|
||
maxQueries: 5,
|
||
enableWebResearch: chatbotEnableWebResearch,
|
||
enableRetryOnEmpty: true,
|
||
maxRetryAttempts: 2
|
||
},
|
||
allowedProviders: chatbotAllowedProviders
|
||
};
|
||
}
|
||
|
||
const result = await createInstance(selectedMandateId, {
|
||
featureCode: createFeatureCode,
|
||
label: createLabel,
|
||
enabled: data.enabled !== false,
|
||
copyTemplateRoles: data.copyTemplateRoles !== false,
|
||
config: config
|
||
});
|
||
if (result.success) {
|
||
setShowCreateModal(false);
|
||
setCreateFeatureCode('');
|
||
setCreateLabel('');
|
||
formDataRef.current = {};
|
||
setChatbotConnectors(['preprocessor']);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotEnableWebResearch(true);
|
||
setChatbotAllowedProviders([]);
|
||
fetchInstances(selectedMandateId);
|
||
loadFeatures(); // Refresh global navigation cache
|
||
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
||
} else {
|
||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
||
}
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Wrapper for form submission that includes featureCode from selector
|
||
const handleFormSubmit = (data: Record<string, any>) => {
|
||
// Use label from state and featureCode from selector
|
||
handleCreateInstance({
|
||
featureCode: createFeatureCode,
|
||
...(data as { enabled?: boolean; copyTemplateRoles?: boolean })
|
||
});
|
||
};
|
||
|
||
// Handle edit click
|
||
const handleEditClick = (instance: FeatureInstance) => {
|
||
setEditingInstance(instance);
|
||
// Load chatbot config if it's a chatbot instance
|
||
if (instance.featureCode === 'chatbot' && instance.config) {
|
||
const config = instance.config as any;
|
||
// Support both new array format and legacy single type format
|
||
// Filter out 'websearch' if it exists (legacy)
|
||
const connectorTypes = (config?.connector?.types || (config?.connector?.type ? [config.connector.type] : ['preprocessor']))
|
||
.filter((c: string) => c !== 'websearch'); // Remove websearch from connectors
|
||
setChatbotConnectors(connectorTypes);
|
||
setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || '');
|
||
setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false);
|
||
setChatbotAllowedProviders(Array.isArray(config?.allowedProviders) ? config.allowedProviders
|
||
: Array.isArray(config?.model?.allowedProviders) ? config.model.allowedProviders : []);
|
||
} else {
|
||
setChatbotConnectors([]);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotEnableWebResearch(true);
|
||
setChatbotAllowedProviders([]);
|
||
}
|
||
setShowEditModal(true);
|
||
};
|
||
|
||
// Handle update instance
|
||
const handleUpdateInstance = async (data: { label: string; enabled?: boolean }) => {
|
||
if (!selectedMandateId || !editingInstance) return;
|
||
setIsSubmitting(true);
|
||
try {
|
||
// Build config for chatbot instances
|
||
let config: Record<string, any> | undefined = undefined;
|
||
if (editingInstance.featureCode === 'chatbot') {
|
||
// Validate required fields
|
||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||
setIsSubmitting(false);
|
||
return;
|
||
}
|
||
const existingConfig = editingInstance.config as any || {};
|
||
const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null;
|
||
config = {
|
||
...existingConfig,
|
||
connector: chatbotConnectors.length > 0 ? {
|
||
types: chatbotConnectors,
|
||
type: primaryConnector,
|
||
customConnectorClass: existingConfig.connector?.customConnectorClass || null
|
||
} : undefined,
|
||
prompts: {
|
||
useCustomPrompts: true, // Always true since system prompt is required
|
||
customAnalysisPrompt: chatbotSystemPrompt,
|
||
customFinalAnswerPrompt: chatbotSystemPrompt
|
||
},
|
||
behavior: {
|
||
...(existingConfig.behavior || {}),
|
||
maxQueries: existingConfig.behavior?.maxQueries || 5,
|
||
enableWebResearch: chatbotEnableWebResearch,
|
||
enableRetryOnEmpty: existingConfig.behavior?.enableRetryOnEmpty !== false,
|
||
maxRetryAttempts: existingConfig.behavior?.maxRetryAttempts || 2
|
||
},
|
||
allowedProviders: chatbotAllowedProviders
|
||
};
|
||
}
|
||
|
||
const result = await updateInstance(selectedMandateId, editingInstance.id, {
|
||
label: data.label,
|
||
enabled: data.enabled,
|
||
config: config
|
||
});
|
||
if (result.success) {
|
||
setShowEditModal(false);
|
||
setEditingInstance(null);
|
||
setChatbotConnectors(['preprocessor']);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotAllowedProviders([]);
|
||
fetchInstances(selectedMandateId);
|
||
loadFeatures(); // Refresh global navigation cache
|
||
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
||
} else {
|
||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
||
}
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Handle delete instance - called by DeleteActionButton with instanceId
|
||
const handleDeleteInstance = async (instanceId: string): Promise<boolean> => {
|
||
if (!selectedMandateId) return false;
|
||
const result = await deleteInstance(selectedMandateId, instanceId);
|
||
if (result.success) {
|
||
loadFeatures(); // Refresh global navigation cache
|
||
showSuccess(t('Instanz gelöscht'), t('Die Feature-Instanz wurde gelöscht.'));
|
||
return true;
|
||
} else {
|
||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Feature-Instanz'));
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// Handle sync roles
|
||
const handleSyncRoles = async (instance: FeatureInstance) => {
|
||
if (!selectedMandateId) return;
|
||
setSyncingInstance(instance.id);
|
||
try {
|
||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||
if (result.success && result.data) {
|
||
showSuccess(
|
||
t('Rollen synchronisiert'),
|
||
t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', {
|
||
added: result.data.added,
|
||
removed: result.data.removed,
|
||
unchanged: result.data.unchanged,
|
||
})
|
||
);
|
||
} else {
|
||
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen'));
|
||
}
|
||
} finally {
|
||
setSyncingInstance(null);
|
||
}
|
||
};
|
||
|
||
// Handle sync workflows
|
||
const _handleSyncWorkflows = async (instance: FeatureInstance) => {
|
||
if (!selectedMandateId) return;
|
||
setSyncingWorkflowsInstance(instance.id);
|
||
try {
|
||
const result = await syncInstanceWorkflows(selectedMandateId, instance.id);
|
||
if (result.success && result.data) {
|
||
showSuccess(
|
||
t('Workflows synchronisiert'),
|
||
t('Hinzugefügt: {added}\nÜbersprungen: {skipped}\nTotal Templates: {total}', {
|
||
added: result.data.added,
|
||
skipped: result.data.skipped,
|
||
total: result.data.total,
|
||
})
|
||
);
|
||
} else {
|
||
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Workflows'));
|
||
}
|
||
} finally {
|
||
setSyncingWorkflowsInstance(null);
|
||
}
|
||
};
|
||
|
||
// Get feature label
|
||
const getFeatureLabel = (code: string) => {
|
||
const feature = features.find(f => f.code === code);
|
||
return feature ? (feature.label || code) : code;
|
||
};
|
||
|
||
if (error && !selectedMandateId) {
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||
<FaSync /> {t('Erneut versuchen')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>{t('Feature-Instanzen')}</h1>
|
||
<p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mandate Selector */}
|
||
<div className={styles.filterSection}>
|
||
<div className={styles.filterGroup}>
|
||
<label className={styles.filterLabel}>
|
||
<FaBuilding style={{ marginRight: 8 }} />
|
||
{t('Mandant auswählen:')}
|
||
</label>
|
||
<select
|
||
className={styles.filterSelect}
|
||
value={selectedMandateId}
|
||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||
>
|
||
<option value="">{t('Mandant wählen')}</option>
|
||
{mandates.map(m => (
|
||
<option key={m.id} value={m.id}>
|
||
{mandateDisplayLabel(m)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{selectedMandateId && (
|
||
<div className={styles.headerActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => fetchInstances(selectedMandateId)}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||
</button>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={() => setShowCreateModal(true)}
|
||
disabled={features.length === 0}
|
||
title={
|
||
features.length === 0
|
||
? t('Keine Features verfügbar. Bitte laden Sie die Seite neu oder prüfen Sie die Konsole auf Fehler.')
|
||
: undefined
|
||
}
|
||
>
|
||
<FaPlus /> {t('Neue Instanz')}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Available Features Info / Empty Features Warning */}
|
||
{features.length > 0 ? (
|
||
<div className={styles.infoBox}>
|
||
<FaCube style={{ marginRight: 8 }} />
|
||
<span>{t('Verfügbare Features')} </span>
|
||
{features.map((f, i) => (
|
||
<span key={f.code}>
|
||
{i > 0 && ', '}
|
||
<strong>{getFeatureLabel(f.code)}</strong>
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : selectedMandateId && !loading ? (
|
||
<div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
|
||
<FaCube style={{ marginRight: 8 }} />
|
||
<span>
|
||
{t('Keine Features geladen.')}
|
||
{error ? ` Fehler: ${error}` : ` ${t('Die API hat keine Features zurückgegeben.')}`}
|
||
{' '}
|
||
{t('Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/')}
|
||
</span>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => fetchFeatures()}
|
||
style={{ marginLeft: '1rem' }}
|
||
>
|
||
<FaSync /> {t('Features erneut laden')}
|
||
</button>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* Content */}
|
||
{!selectedMandateId ? (
|
||
<div className={styles.emptyState}>
|
||
<FaBuilding className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||
<p className={styles.emptyDescription}>
|
||
{t('Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.')}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className={styles.tableContainer}>
|
||
<FormGeneratorTable
|
||
data={instances}
|
||
columns={columns}
|
||
apiEndpoint="/api/features/instances"
|
||
loading={loading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={true}
|
||
actionButtons={[
|
||
{
|
||
type: 'delete' as const,
|
||
title: t('Instanz löschen'),
|
||
}
|
||
]}
|
||
customActions={[
|
||
{
|
||
id: 'edit',
|
||
icon: <FaEdit />,
|
||
onClick: handleEditClick,
|
||
title: t('Instanz bearbeiten'),
|
||
},
|
||
{
|
||
id: 'syncRoles',
|
||
icon: <FaCogs />,
|
||
onClick: handleSyncRoles,
|
||
title: t('Rollen synchronisieren'),
|
||
loading: (row: FeatureInstance) => syncingInstance === row.id,
|
||
disabled: (row: FeatureInstance) => !row.enabled,
|
||
},
|
||
{
|
||
id: 'syncWorkflows',
|
||
icon: <FaSync />,
|
||
onClick: _handleSyncWorkflows,
|
||
title: t('Workflows synchronisieren'),
|
||
loading: (row: FeatureInstance) => syncingWorkflowsInstance === row.id,
|
||
disabled: (row: FeatureInstance) => !row.enabled,
|
||
}
|
||
]}
|
||
hookData={{
|
||
refetch: fetchInstances,
|
||
pagination: instancesPagination,
|
||
handleDelete: handleDeleteInstance,
|
||
}}
|
||
emptyMessage={t('Keine Feature-Instanzen gefunden')}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create Instance Modal */}
|
||
{showCreateModal && (
|
||
<div className={styles.modalOverlay}>
|
||
<div className={styles.modal}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz erstellen')}</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => setShowCreateModal(false)}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{features.length === 0 ? (
|
||
<p>{t('Keine Features verfügbar, bitte wenden')}</p>
|
||
) : createFields.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>{t('Lade Formular')}</span>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
{/* Feature Code Selector — buttons instead of dropdown */}
|
||
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
|
||
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
|
||
{t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||
</label>
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||
{features.map(f => (
|
||
<button
|
||
key={f.code}
|
||
type="button"
|
||
className={styles.secondaryButton}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
borderRadius: '6px',
|
||
cursor: 'pointer',
|
||
fontWeight: createFeatureCode === f.code ? 600 : 400,
|
||
background: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
|
||
color: createFeatureCode === f.code ? '#fff' : undefined,
|
||
borderColor: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
|
||
}}
|
||
onClick={() => {
|
||
setCreateFeatureCode(f.code);
|
||
setChatbotConnectors(['preprocessor']);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotEnableWebResearch(true);
|
||
setChatbotAllowedProviders([]);
|
||
}}
|
||
>
|
||
{f.label || f.code}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Chatbot Configuration Title - Show when chatbot is selected */}
|
||
{createFeatureCode === 'chatbot' && (
|
||
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
||
{t('Chatbot-Konfiguration')}
|
||
</h3>
|
||
)}
|
||
|
||
{/* Label Field - Always shown after title */}
|
||
{createFeatureCode && (
|
||
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
|
||
<label className={styles.configLabel}>
|
||
{t('Label')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||
</label>
|
||
<TextField
|
||
type="text"
|
||
value={createLabel}
|
||
onChange={(value) => setCreateLabel(value)}
|
||
placeholder={t('Instanzbezeichnung eingeben')}
|
||
className={styles.configSelect}
|
||
size="md"
|
||
required={true}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Chatbot Configuration Section - Show when chatbot is selected */}
|
||
{createFeatureCode === 'chatbot' && (
|
||
<ChatbotConfigSection
|
||
connectors={chatbotConnectors}
|
||
systemPrompt={chatbotSystemPrompt}
|
||
enableWebResearch={chatbotEnableWebResearch}
|
||
allowedProviders={chatbotAllowedProviders}
|
||
onConnectorsChange={setChatbotConnectors}
|
||
onSystemPromptChange={setChatbotSystemPrompt}
|
||
onEnableWebResearchChange={setChatbotEnableWebResearch}
|
||
onAllowedProvidersChange={setChatbotAllowedProviders}
|
||
/>
|
||
)}
|
||
|
||
{/* Main Form - Only show if featureCode is selected */}
|
||
{createFeatureCode && (
|
||
<div style={{ marginTop: '1.5rem' }}>
|
||
<FormGeneratorForm
|
||
attributes={createFields}
|
||
mode="create"
|
||
onSubmit={handleFormSubmit}
|
||
onCancel={() => {
|
||
setShowCreateModal(false);
|
||
setCreateFeatureCode('');
|
||
setCreateLabel('');
|
||
formDataRef.current = {};
|
||
setChatbotConnectors(['preprocessor']);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotEnableWebResearch(true);
|
||
setChatbotAllowedProviders([]);
|
||
}}
|
||
submitButtonText={t('Erstellen')}
|
||
cancelButtonText={t('Abbrechen')}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Instance Modal */}
|
||
{showEditModal && editingInstance && (
|
||
<div className={styles.modalOverlay}>
|
||
<div className={styles.modal}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>{t('Feature-Instanz bearbeiten')}</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => { setShowEditModal(false); setEditingInstance(null); }}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
<FormGeneratorForm
|
||
attributes={[
|
||
{
|
||
name: 'label',
|
||
type: 'string' as const,
|
||
label: t('Bezeichnung'),
|
||
required: true,
|
||
editable: true,
|
||
},
|
||
{
|
||
name: 'enabled',
|
||
type: 'boolean' as const,
|
||
label: t('Aktiviert'),
|
||
required: false,
|
||
editable: true,
|
||
}
|
||
]}
|
||
data={editingInstance}
|
||
mode="edit"
|
||
onSubmit={handleUpdateInstance}
|
||
onCancel={() => {
|
||
setShowEditModal(false);
|
||
setEditingInstance(null);
|
||
setChatbotConnectors(['preprocessor']);
|
||
setChatbotSystemPrompt('');
|
||
setChatbotEnableWebResearch(true);
|
||
setChatbotAllowedProviders([]);
|
||
}}
|
||
submitButtonText={t('Speichern')}
|
||
cancelButtonText={t('Abbrechen')}
|
||
/>
|
||
|
||
{/* Chatbot Configuration Section */}
|
||
{editingInstance?.featureCode === 'chatbot' && (
|
||
<ChatbotConfigSection
|
||
connectors={chatbotConnectors}
|
||
systemPrompt={chatbotSystemPrompt}
|
||
enableWebResearch={chatbotEnableWebResearch}
|
||
allowedProviders={chatbotAllowedProviders}
|
||
onConnectorsChange={setChatbotConnectors}
|
||
onSystemPromptChange={setChatbotSystemPrompt}
|
||
onEnableWebResearchChange={setChatbotEnableWebResearch}
|
||
onAllowedProvidersChange={setChatbotAllowedProviders}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AdminFeatureAccessPage;
|