fixes
This commit is contained in:
parent
70459d57e3
commit
ad96c6d861
3 changed files with 324 additions and 1 deletions
|
|
@ -332,6 +332,7 @@ const _LEGACY_RENDERERS_THAT_HANDLE_BINDINGS = new Set([
|
|||
'filterExpression',
|
||||
'attachmentBuilder',
|
||||
'json',
|
||||
'modelMultiSelect',
|
||||
]);
|
||||
|
||||
function _schemaNamesFromOutputPort(def: { schema?: string | GraphDefinedSchemaRef } | undefined): string[] {
|
||||
|
|
|
|||
|
|
@ -737,6 +737,113 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
|
|||
);
|
||||
};
|
||||
|
||||
const ModelMultiSelect: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const [models, setModels] = React.useState<Array<{ displayName: string; connectorType?: string }>>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const selected: string[] = Array.isArray(value) ? value : [];
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
fetch('/api/system/ai-models', { credentials: 'include' })
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const items = (data?.models ?? []) as Array<{ displayName: string; connectorType?: string }>;
|
||||
setModels(items);
|
||||
})
|
||||
.catch(() => { if (!cancelled) setModels([]); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const _toggle = (name: string) => {
|
||||
const next = selected.includes(name)
|
||||
? selected.filter((v) => v !== name)
|
||||
: [...selected, name];
|
||||
onChange(next);
|
||||
};
|
||||
|
||||
const _removeTag = (name: string) => {
|
||||
onChange(selected.filter((v) => v !== name));
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
<div
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 32,
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #ccc',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
{selected.length === 0 && (
|
||||
<span style={{ color: '#999', fontSize: 12 }}>{t('Alle erlaubten Modelle')}</span>
|
||||
)}
|
||||
{selected.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
style={{
|
||||
background: 'var(--primary-color, #2563eb)',
|
||||
color: '#fff',
|
||||
borderRadius: 3,
|
||||
padding: '1px 6px',
|
||||
fontSize: 11,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); _removeTag(name); }}
|
||||
style={{ cursor: 'pointer', fontWeight: 700 }}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{open && (
|
||||
<div style={{ border: '1px solid #ddd', borderRadius: 4, marginTop: 4, maxHeight: 200, overflow: 'auto', background: '#fafafa', padding: 4 }}>
|
||||
{loading && <div style={{ fontSize: 11, color: '#888', padding: 4 }}>{t('Lade Modelle...')}</div>}
|
||||
{!loading && models.length === 0 && (
|
||||
<div style={{ fontSize: 11, color: '#888', padding: 4 }}>{t('Keine Modelle verfügbar')}</div>
|
||||
)}
|
||||
{models.map((m) => (
|
||||
<label
|
||||
key={m.displayName}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 4px', fontSize: 12, cursor: 'pointer' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(m.displayName)}
|
||||
onChange={() => _toggle(m.displayName)}
|
||||
/>
|
||||
<span>{m.displayName}</span>
|
||||
{m.connectorType && (
|
||||
<span style={{ fontSize: 10, color: '#888' }}>({m.connectorType})</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -752,6 +859,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
|
|||
email: TextInput,
|
||||
select: SelectInput,
|
||||
multiselect: MultiSelectInput,
|
||||
modelMultiSelect: ModelMultiSelect,
|
||||
json: JsonEditor,
|
||||
file: TextInput,
|
||||
hidden: HiddenInput,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ interface MaxAgentRoundsInfo {
|
|||
instanceDefault: number;
|
||||
}
|
||||
|
||||
interface AiUserSettings {
|
||||
requireNeutralization: boolean;
|
||||
allowedProviders: string[];
|
||||
allowedModels: string[];
|
||||
}
|
||||
|
||||
interface AiModelEntry {
|
||||
displayName: string;
|
||||
connectorType?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ instanceId }) => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
|
|
@ -36,6 +47,16 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
});
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
|
||||
// AI user settings
|
||||
const [aiSettings, setAiSettings] = useState<AiUserSettings>({
|
||||
requireNeutralization: false,
|
||||
allowedProviders: [],
|
||||
allowedModels: [],
|
||||
});
|
||||
const [aiSaving, setAiSaving] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<AiModelEntry[]>([]);
|
||||
const [modelsOpen, setModelsOpen] = useState(false);
|
||||
|
||||
const _loadSettings = useCallback(async () => {
|
||||
if (!instanceId) return;
|
||||
setLoading(true);
|
||||
|
|
@ -56,9 +77,37 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
}
|
||||
}, [instanceId, request]);
|
||||
|
||||
const _loadAiSettings = useCallback(async () => {
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/workspace/${instanceId}/user-settings`,
|
||||
method: 'get',
|
||||
}) as AiUserSettings;
|
||||
setAiSettings({
|
||||
requireNeutralization: data?.requireNeutralization ?? false,
|
||||
allowedProviders: data?.allowedProviders ?? [],
|
||||
allowedModels: data?.allowedModels ?? [],
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('[WorkspaceGeneralSettings] AI settings load failed', err);
|
||||
}
|
||||
}, [instanceId, request]);
|
||||
|
||||
const _loadAvailableModels = useCallback(async () => {
|
||||
try {
|
||||
const data = await request({ url: '/api/system/ai-models', method: 'get' }) as { models?: AiModelEntry[] };
|
||||
setAvailableModels(data?.models ?? []);
|
||||
} catch {
|
||||
setAvailableModels([]);
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
useEffect(() => {
|
||||
_loadSettings();
|
||||
}, [_loadSettings]);
|
||||
_loadAiSettings();
|
||||
_loadAvailableModels();
|
||||
}, [_loadSettings, _loadAiSettings, _loadAvailableModels]);
|
||||
|
||||
const _handleSave = async () => {
|
||||
setSaving(true);
|
||||
|
|
@ -94,11 +143,49 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
setInputValue('');
|
||||
};
|
||||
|
||||
const _saveAiSettings = async (patch: Partial<AiUserSettings>) => {
|
||||
setAiSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/workspace/${instanceId}/user-settings`,
|
||||
method: 'put',
|
||||
data: patch,
|
||||
}) as AiUserSettings;
|
||||
setAiSettings({
|
||||
requireNeutralization: data?.requireNeutralization ?? false,
|
||||
allowedProviders: data?.allowedProviders ?? [],
|
||||
allowedModels: data?.allowedModels ?? [],
|
||||
});
|
||||
setSuccess(t('KI-Einstellungen gespeichert'));
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Fehler beim Speichern der KI-Einstellungen');
|
||||
} finally {
|
||||
setAiSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _toggleModel = (name: string) => {
|
||||
const next = aiSettings.allowedModels.includes(name)
|
||||
? aiSettings.allowedModels.filter((m) => m !== name)
|
||||
: [...aiSettings.allowedModels, name];
|
||||
setAiSettings((prev) => ({ ...prev, allowedModels: next }));
|
||||
_saveAiSettings({ allowedModels: next });
|
||||
};
|
||||
|
||||
const _removeModelTag = (name: string) => {
|
||||
const next = aiSettings.allowedModels.filter((m) => m !== name);
|
||||
setAiSettings((prev) => ({ ...prev, allowedModels: next }));
|
||||
_saveAiSettings({ allowedModels: next });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className={styles.loading}>{t('Lade Einstellungen')}</div>;
|
||||
}
|
||||
|
||||
const hasOverride = inputValue.trim() !== '';
|
||||
const providerNames = [...new Set(availableModels.map((m) => m.connectorType).filter(Boolean))] as string[];
|
||||
|
||||
return (
|
||||
<div className={styles.settings}>
|
||||
|
|
@ -151,6 +238,133 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
>
|
||||
{saving ? t('Speichern') : t('Einstellungen speichern')}
|
||||
</button>
|
||||
|
||||
{/* AI settings section */}
|
||||
<div className={styles.section} style={{ marginTop: '1.5rem' }}>
|
||||
<h3 className={styles.sectionTitle}>{t('KI-Einstellungen')}</h3>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aiSettings.requireNeutralization}
|
||||
onChange={(e) => {
|
||||
const val = e.target.checked;
|
||||
setAiSettings((prev) => ({ ...prev, requireNeutralization: val }));
|
||||
_saveAiSettings({ requireNeutralization: val });
|
||||
}}
|
||||
disabled={aiSaving}
|
||||
/>
|
||||
<span className={styles.label} style={{ marginBottom: 0 }}>
|
||||
{t('Neutralisierung erzwingen')}
|
||||
</span>
|
||||
</label>
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', marginTop: 4, display: 'block' }}>
|
||||
{t('Erzwingt die Neutralisierung von Eingaben vor der KI-Verarbeitung.')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{providerNames.length > 0 && (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{t('Erlaubte Anbieter')}</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{providerNames.map((prov) => (
|
||||
<label key={prov} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: '0.85rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aiSettings.allowedProviders.includes(prov)}
|
||||
onChange={() => {
|
||||
const next = aiSettings.allowedProviders.includes(prov)
|
||||
? aiSettings.allowedProviders.filter((p) => p !== prov)
|
||||
: [...aiSettings.allowedProviders, prov];
|
||||
setAiSettings((prev) => ({ ...prev, allowedProviders: next }));
|
||||
_saveAiSettings({ allowedProviders: next });
|
||||
}}
|
||||
disabled={aiSaving}
|
||||
/>
|
||||
{prov}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{aiSettings.allowedProviders.length === 0 && (
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', marginTop: 4, display: 'block' }}>
|
||||
{t('Alle Anbieter erlaubt')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{t('Erlaubte Modelle')}</label>
|
||||
<div
|
||||
onClick={() => setModelsOpen((o) => !o)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 36,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid var(--border-color, #d0d0d0)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
background: 'var(--bg-primary, #fff)',
|
||||
}}
|
||||
>
|
||||
{aiSettings.allowedModels.length === 0 && (
|
||||
<span style={{ color: 'var(--text-secondary, #999)', fontSize: '0.85rem' }}>{t('Alle erlaubten Modelle')}</span>
|
||||
)}
|
||||
{aiSettings.allowedModels.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
style={{
|
||||
background: 'var(--primary-color, #2563eb)',
|
||||
color: '#fff',
|
||||
borderRadius: 4,
|
||||
padding: '2px 8px',
|
||||
fontSize: '0.8rem',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); _removeModelTag(name); }}
|
||||
style={{ cursor: 'pointer', fontWeight: 700 }}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{modelsOpen && (
|
||||
<div style={{ border: '1px solid var(--border-color, #ddd)', borderRadius: 6, marginTop: 4, maxHeight: 220, overflow: 'auto', background: 'var(--bg-primary, #fafafa)', padding: 6 }}>
|
||||
{availableModels.length === 0 && (
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', padding: 4 }}>{t('Keine Modelle verfügbar')}</div>
|
||||
)}
|
||||
{availableModels.map((m) => (
|
||||
<label
|
||||
key={m.displayName}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 6px', fontSize: '0.85rem', cursor: 'pointer' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aiSettings.allowedModels.includes(m.displayName)}
|
||||
onChange={() => _toggleModel(m.displayName)}
|
||||
disabled={aiSaving}
|
||||
/>
|
||||
<span>{m.displayName}</span>
|
||||
{m.connectorType && (
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #888)' }}>({m.connectorType})</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue