fixed onboarding flow
This commit is contained in:
parent
9d4e5bc90d
commit
9ea6ed4613
12 changed files with 308 additions and 241 deletions
|
|
@ -14,7 +14,9 @@ import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
||||||
import { FaSave, FaChevronLeft, FaChevronRight, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa';
|
import { FaSave, FaChevronLeft, FaChevronRight, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa';
|
||||||
import { Popup } from '../UiComponents/Popup';
|
import { Popup } from '../UiComponents/Popup';
|
||||||
import { ActionsPanel } from '../ActionsPanel';
|
import { ActionsPanel } from '../ActionsPanel';
|
||||||
import { ProviderMultiSelect } from '../ProviderSelector';
|
import { ProviderMultiSelect, _defaultProviderSelection, _migrateFromLegacy, _toBackendProviders } from '../ProviderSelector';
|
||||||
|
import type { ProviderSelection } from '../ProviderSelector';
|
||||||
|
import { useBilling } from '../../hooks/useBilling';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
import { useWorkflowActions } from '../../hooks/useAutomations';
|
import { useWorkflowActions } from '../../hooks/useAutomations';
|
||||||
|
|
@ -374,7 +376,8 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||||
const [label, setLabel] = useState('');
|
const [label, setLabel] = useState('');
|
||||||
const [schedule, setSchedule] = useState('0 22 * * *');
|
const [schedule, setSchedule] = useState('0 22 * * *');
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const [allowedProviders, setAllowedProviders] = useState<string[]>([]);
|
const [providerSelection, setProviderSelection] = useState<ProviderSelection>(_defaultProviderSelection());
|
||||||
|
const { allowedProviders: billingProviders } = useBilling();
|
||||||
|
|
||||||
// Template multilingual fields
|
// Template multilingual fields
|
||||||
const [labelMulti, setLabelMulti] = useState<LocalTextMultilingual>({ en: '', de: '' });
|
const [labelMulti, setLabelMulti] = useState<LocalTextMultilingual>({ en: '', de: '' });
|
||||||
|
|
@ -537,7 +540,7 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||||
setLabel(def.label || '');
|
setLabel(def.label || '');
|
||||||
setSchedule(def.schedule || '0 22 * * *');
|
setSchedule(def.schedule || '0 22 * * *');
|
||||||
setActive(def.active ?? false);
|
setActive(def.active ?? false);
|
||||||
setAllowedProviders(def.allowedProviders || []);
|
setProviderSelection(_migrateFromLegacy(def.allowedProviders || []));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract template JSON
|
// Extract template JSON
|
||||||
|
|
@ -693,7 +696,7 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||||
active,
|
active,
|
||||||
template: templateJson,
|
template: templateJson,
|
||||||
placeholders,
|
placeholders,
|
||||||
allowedProviders
|
allowedProviders: _toBackendProviders(providerSelection, billingProviders),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,7 +712,7 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [label, schedule, active, allowedProviders, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
|
}, [label, schedule, active, providerSelection, billingProviders, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
|
||||||
|
|
||||||
// Computed values
|
// Computed values
|
||||||
const editorTitle = title || (mode === 'template'
|
const editorTitle = title || (mode === 'template'
|
||||||
|
|
@ -864,12 +867,12 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||||
{/* Allowed AI Providers */}
|
{/* Allowed AI Providers */}
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<ProviderMultiSelect
|
<ProviderMultiSelect
|
||||||
selectedProviders={allowedProviders}
|
selection={providerSelection}
|
||||||
onChange={setAllowedProviders}
|
onChange={setProviderSelection}
|
||||||
label="Erlaubte AI-Provider"
|
label="Erlaubte AI-Provider"
|
||||||
/>
|
/>
|
||||||
<p className={styles.formHint}>
|
<p className={styles.formHint}>
|
||||||
Beschränkt die Automation auf bestimmte AI-Provider. Leer = alle erlaubt.
|
Beschränkt die Automation auf bestimmte AI-Provider. «Alle» = dynamisch alle erlaubten.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import OnboardingWizard from './OnboardingWizard';
|
||||||
|
|
||||||
interface OnboardingStep {
|
interface OnboardingStep {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -17,7 +18,7 @@ interface OnboardingAssistantProps {
|
||||||
const _STORAGE_KEY = 'onboarding_hidden';
|
const _STORAGE_KEY = 'onboarding_hidden';
|
||||||
|
|
||||||
const _CALLOUTS: Record<string, string> = {
|
const _CALLOUTS: Record<string, string> = {
|
||||||
mandate: 'Tipp: Ein Mandant ist Ihr persoenlicher Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.',
|
mandate: 'Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.',
|
||||||
feature: 'Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.',
|
feature: 'Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.',
|
||||||
connection: 'Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.',
|
connection: 'Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.',
|
||||||
chat: 'Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.',
|
chat: 'Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.',
|
||||||
|
|
@ -50,46 +51,59 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||||
|
const [showWizard, setShowWizard] = useState(false);
|
||||||
|
|
||||||
const _checkOnboardingState = useCallback(async () => {
|
const _checkOnboardingState = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const onboardingSteps: OnboardingStep[] = [];
|
const onboardingSteps: OnboardingStep[] = [];
|
||||||
|
|
||||||
let hasMandate = false;
|
// Check admin mandates (user-owned or where user is admin)
|
||||||
|
let hasAdminMandate = false;
|
||||||
try {
|
try {
|
||||||
const mandatesRes = await api.get('/api/store/mandates');
|
const mandatesRes = await api.get('/api/store/mandates');
|
||||||
const mandates = mandatesRes.data?.mandates || mandatesRes.data || [];
|
const mandates = mandatesRes.data?.mandates || mandatesRes.data || [];
|
||||||
hasMandate = Array.isArray(mandates) && mandates.length > 0;
|
hasAdminMandate = Array.isArray(mandates) && mandates.length > 0;
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
onboardingSteps.push({
|
// Check if user has any feature access (via navigation = mandate member)
|
||||||
id: 'mandate',
|
|
||||||
label: 'Mandant einrichten',
|
|
||||||
description: hasMandate
|
|
||||||
? 'Dein Mandant ist eingerichtet.'
|
|
||||||
: 'Richte deinen ersten Mandanten ein.',
|
|
||||||
completed: hasMandate,
|
|
||||||
action: hasMandate ? undefined : () => navigate('/store'),
|
|
||||||
});
|
|
||||||
|
|
||||||
let hasFeature = false;
|
let hasFeature = false;
|
||||||
let firstInstancePath: string | undefined;
|
let workspaceInstancePath: string | undefined;
|
||||||
|
let workspaceInstanceIds: string[] = [];
|
||||||
try {
|
try {
|
||||||
const navRes = await api.get('/api/navigation?language=de');
|
const navRes = await api.get('/api/navigation?language=de');
|
||||||
const mandates = navRes.data?.mandates || [];
|
const blocks = navRes.data?.blocks || [];
|
||||||
|
const dynamicBlock = blocks.find((b: { type: string }) => b.type === 'dynamic');
|
||||||
|
const mandates = dynamicBlock?.mandates || [];
|
||||||
for (const m of mandates) {
|
for (const m of mandates) {
|
||||||
for (const f of m.features || []) {
|
for (const f of m.features || []) {
|
||||||
for (const inst of f.instances || []) {
|
for (const inst of f.instances || []) {
|
||||||
if (!hasFeature) hasFeature = true;
|
hasFeature = true;
|
||||||
if (!firstInstancePath && inst.views?.length > 0) {
|
if (f.uiComponent === 'feature.workspace' && inst.views?.length > 0) {
|
||||||
firstInstancePath = inst.views[0].uiPath;
|
workspaceInstanceIds.push(inst.id);
|
||||||
|
if (!workspaceInstancePath) {
|
||||||
|
workspaceInstancePath = inst.views[0].uiPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
const mandateStepDone = hasAdminMandate || hasFeature;
|
||||||
|
|
||||||
|
onboardingSteps.push({
|
||||||
|
id: 'mandate',
|
||||||
|
label: 'Mandant einrichten',
|
||||||
|
description: hasAdminMandate
|
||||||
|
? 'Dein Mandant ist eingerichtet.'
|
||||||
|
: hasFeature
|
||||||
|
? 'Du bist Mitglied eines Mandanten.'
|
||||||
|
: 'Erstelle deinen Arbeitsbereich.',
|
||||||
|
completed: mandateStepDone,
|
||||||
|
action: mandateStepDone ? undefined : () => setShowWizard(true),
|
||||||
|
});
|
||||||
|
|
||||||
onboardingSteps.push({
|
onboardingSteps.push({
|
||||||
id: 'feature',
|
id: 'feature',
|
||||||
label: 'Erstes Feature aktivieren',
|
label: 'Erstes Feature aktivieren',
|
||||||
|
|
@ -103,8 +117,8 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
let hasConnection = false;
|
let hasConnection = false;
|
||||||
try {
|
try {
|
||||||
const connRes = await api.get('/api/connections/');
|
const connRes = await api.get('/api/connections/');
|
||||||
const connections = connRes.data?.data || connRes.data || [];
|
const items = connRes.data?.items || connRes.data?.data || connRes.data || [];
|
||||||
hasConnection = Array.isArray(connections) && connections.length > 0;
|
hasConnection = Array.isArray(items) && items.length > 0;
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
onboardingSteps.push({
|
onboardingSteps.push({
|
||||||
|
|
@ -118,25 +132,16 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasChat = false;
|
let hasChat = false;
|
||||||
if (hasFeature && firstInstancePath) {
|
for (const instId of workspaceInstanceIds) {
|
||||||
|
if (hasChat) break;
|
||||||
try {
|
try {
|
||||||
const featuresRes = await api.get('/api/store/features');
|
const wfRes = await api.get(`/api/workspace/${instId}/workflows`);
|
||||||
const features = featuresRes.data || [];
|
const wfs = wfRes.data?.workflows || wfRes.data?.data || wfRes.data?.items || [];
|
||||||
for (const f of features) {
|
if (Array.isArray(wfs) && wfs.length > 0) hasChat = true;
|
||||||
if (hasChat) break;
|
|
||||||
for (const inst of f.instances || []) {
|
|
||||||
if (hasChat) break;
|
|
||||||
try {
|
|
||||||
const wfRes = await api.get(`/api/workspace/${inst.id}/workflows`);
|
|
||||||
const wfs = wfRes.data?.workflows || wfRes.data?.data || [];
|
|
||||||
if (Array.isArray(wfs) && wfs.length > 0) hasChat = true;
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const _chatAction = firstInstancePath ? () => navigate(firstInstancePath!) : undefined;
|
const chatAction = workspaceInstancePath ? () => navigate(workspaceInstancePath!) : undefined;
|
||||||
onboardingSteps.push({
|
onboardingSteps.push({
|
||||||
id: 'chat',
|
id: 'chat',
|
||||||
label: 'Ersten AI-Chat starten',
|
label: 'Ersten AI-Chat starten',
|
||||||
|
|
@ -144,7 +149,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
? 'Du hast bereits Chats gestartet.'
|
? 'Du hast bereits Chats gestartet.'
|
||||||
: 'Starte deinen ersten Chat mit dem AI-Assistenten.',
|
: 'Starte deinen ersten Chat mit dem AI-Assistenten.',
|
||||||
completed: hasChat,
|
completed: hasChat,
|
||||||
action: hasChat ? undefined : _chatAction,
|
action: hasChat ? undefined : chatAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSteps(onboardingSteps);
|
setSteps(onboardingSteps);
|
||||||
|
|
@ -180,6 +185,18 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
||||||
onDismiss?.();
|
onDismiss?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (showWizard) {
|
||||||
|
return (
|
||||||
|
<OnboardingWizard
|
||||||
|
onComplete={() => {
|
||||||
|
setShowWizard(false);
|
||||||
|
_checkOnboardingState();
|
||||||
|
}}
|
||||||
|
onDismiss={() => setShowWizard(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (hidden || loading) return null;
|
if (hidden || loading) return null;
|
||||||
|
|
||||||
const completedCount = steps.filter(s => s.completed).length;
|
const completedCount = steps.filter(s => s.completed).length;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ interface OnboardingWizardProps {
|
||||||
|
|
||||||
const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismiss }) => {
|
const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismiss }) => {
|
||||||
const [planKey, setPlanKey] = useState<'TRIAL_7D' | 'STANDARD_MONTHLY'>('TRIAL_7D');
|
const [planKey, setPlanKey] = useState<'TRIAL_7D' | 'STANDARD_MONTHLY'>('TRIAL_7D');
|
||||||
const [companyName, setCompanyName] = useState('');
|
const [mandateName, setMandateName] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -16,10 +16,15 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await api.post('/api/local/onboarding', {
|
const res = await api.post('/api/local/onboarding', {
|
||||||
planKey,
|
planKey,
|
||||||
companyName: companyName.trim() || undefined,
|
companyName: mandateName.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
if (res.data?.alreadyProvisioned) {
|
||||||
|
setError('Du hast bereits einen Mandanten mit Admin-Zugang.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung');
|
setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung');
|
||||||
|
|
@ -38,9 +43,9 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
background: 'var(--bg-primary, #fff)', borderRadius: '12px', padding: '32px',
|
background: 'var(--bg-primary, #fff)', borderRadius: '12px', padding: '32px',
|
||||||
maxWidth: '480px', width: '90%', boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
maxWidth: '480px', width: '90%', boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>Willkommen bei PowerOn</h2>
|
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>Mandant erstellen</h2>
|
||||||
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
||||||
Wähle dein Abo und leg los.
|
Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
||||||
|
|
@ -80,8 +85,8 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
Name des Mandanten <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>(optional)</span>
|
Name des Mandanten <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text" value={companyName}
|
type="text" value={mandateName}
|
||||||
onChange={(e) => setCompanyName(e.target.value)}
|
onChange={(e) => setMandateName(e.target.value)}
|
||||||
placeholder="z. B. Firmenname oder Projektname"
|
placeholder="z. B. Firmenname oder Projektname"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '10px 12px', borderRadius: '6px',
|
width: '100%', padding: '10px 12px', borderRadius: '6px',
|
||||||
|
|
@ -98,7 +103,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
|
padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
|
||||||
background: 'transparent', cursor: 'pointer',
|
background: 'transparent', cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
Später
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button onClick={_handleSubmit} disabled={loading}
|
<button onClick={_handleSubmit} disabled={loading}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -106,7 +111,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
||||||
background: 'var(--accent, #4f46e5)', color: '#fff', cursor: 'pointer',
|
background: 'var(--accent, #4f46e5)', color: '#fff', cursor: 'pointer',
|
||||||
opacity: loading ? 0.6 : 1,
|
opacity: loading ? 0.6 : 1,
|
||||||
}}>
|
}}>
|
||||||
{loading ? 'Wird eingerichtet...' : 'Loslegen'}
|
{loading ? 'Wird eingerichtet...' : 'Mandant erstellen'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,17 +53,17 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--surface-color, #2d2d2d);
|
background: var(--surface-color, #ffffff);
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #666666);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerButton:hover:not(:disabled) {
|
.triggerButton:hover:not(:disabled) {
|
||||||
background: var(--bg-secondary, #3a3a3a);
|
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||||
color: var(--text-primary, #fff);
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerButton:disabled {
|
.triggerButton:disabled {
|
||||||
|
|
@ -83,20 +83,20 @@
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: var(--surface-color, #2d2d2d);
|
background: var(--surface-color, #ffffff);
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownHeader {
|
.dropdownHeader {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #666666);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-bottom: 1px solid var(--border-color, #3a3a3a);
|
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectActions {
|
.selectActions {
|
||||||
|
|
@ -108,18 +108,18 @@
|
||||||
.actionButton {
|
.actionButton {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--bg-secondary, #252525);
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #666666);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton:hover:not(:disabled) {
|
.actionButton:hover:not(:disabled) {
|
||||||
background: var(--bg-hover, #3a3a3a);
|
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||||
color: var(--text-primary, #fff);
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton.active {
|
.actionButton.active {
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: var(--bg-secondary, #252525);
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -151,12 +151,13 @@
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxItem:hover {
|
.checkboxItem:hover {
|
||||||
background: var(--bg-hover, #3a3a3a);
|
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||||
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxItem.disabled {
|
.checkboxItem.disabled {
|
||||||
|
|
@ -177,12 +178,12 @@
|
||||||
|
|
||||||
.providerName {
|
.providerName {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--text-tertiary, #666);
|
color: var(--text-tertiary, #888888);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
@ -192,10 +193,24 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #666666);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark theme: list hover stays a light lift, not a black wash */
|
||||||
|
:global(.dark-theme) .checkboxItem:hover {
|
||||||
|
background: var(--hover-bg, rgba(255, 255, 255, 0.08));
|
||||||
|
color: var(--text-primary, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark-theme) .checkboxItem {
|
||||||
|
color: var(--text-primary, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark-theme) .dropdownContent {
|
||||||
|
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
PROVIDER BADGES
|
PROVIDER BADGES
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,80 @@
|
||||||
/**
|
/**
|
||||||
* ProviderSelector Component
|
* ProviderSelector Component
|
||||||
*
|
*
|
||||||
* Wiederverwendbare Komponente zur Auswahl von AICore-Providern.
|
* Wiederverwendbare Komponente zur Auswahl von AICore-Providern.
|
||||||
* Kann im AI Workspace und Automation Editor verwendet werden.
|
* Kann im AI Workspace und Automation Editor verwendet werden.
|
||||||
*
|
*
|
||||||
* Features:
|
* Selektionsmodell:
|
||||||
* - Dropdown für Einzelauswahl
|
* ProviderSelection { include: string[], exclude: string[] }
|
||||||
* - Checkbox-Liste für Mehrfachauswahl
|
* - include(["ALL"]), exclude([]) → alle verfügbaren Provider (dynamisch)
|
||||||
* - Lädt verfügbare Provider aus dem Billing-System
|
* - 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 React, { useEffect, useMemo, useState, useRef, useCallback } from 'react';
|
||||||
import { useBilling } from '../../hooks/useBilling';
|
import { useBilling } from '../../hooks/useBilling';
|
||||||
import styles from './ProviderSelector.module.css';
|
import styles from './ProviderSelector.module.css';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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
|
// Provider display names
|
||||||
const PROVIDER_LABELS: Record<string, string> = {
|
const PROVIDER_LABELS: Record<string, string> = {
|
||||||
anthropic: 'Anthropic (Claude)',
|
anthropic: 'Anthropic (Claude)',
|
||||||
|
|
@ -25,7 +86,6 @@ const PROVIDER_LABELS: Record<string, string> = {
|
||||||
internal: 'Internal',
|
internal: 'Internal',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Provider icons (emojis for simplicity)
|
|
||||||
const PROVIDER_ICONS: Record<string, string> = {
|
const PROVIDER_ICONS: Record<string, string> = {
|
||||||
anthropic: '🤖',
|
anthropic: '🤖',
|
||||||
openai: '💬',
|
openai: '💬',
|
||||||
|
|
@ -58,20 +118,20 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowedProviders.length === 0 && !loading) {
|
if (allowedProviders.length === 0 && !loading) {
|
||||||
loadAllowedProviders();
|
loadAllowedProviders();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const providerOptions = useMemo(() => {
|
const providerOptions = useMemo(() => {
|
||||||
return allowedProviders.map((provider) => ({
|
return allowedProviders.map((provider) => ({
|
||||||
value: provider,
|
value: provider,
|
||||||
label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`,
|
label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`,
|
||||||
}));
|
}));
|
||||||
}, [allowedProviders]);
|
}, [allowedProviders]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.providerSelect} ${className || ''}`}>
|
<div className={`${styles.providerSelect} ${className || ''}`}>
|
||||||
{showLabel && <label className={styles.label}>{label}</label>}
|
{showLabel && <label className={styles.label}>{label}</label>}
|
||||||
|
|
@ -93,12 +153,12 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MULTI SELECT COMPONENT (Checkbox List)
|
// MULTI SELECT COMPONENT (Checkbox List) — include / exclude model
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface ProviderMultiSelectProps {
|
interface ProviderMultiSelectProps {
|
||||||
selectedProviders: string[];
|
selection: ProviderSelection;
|
||||||
onChange: (providers: string[]) => void;
|
onChange: (selection: ProviderSelection) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|
@ -108,7 +168,7 @@ interface ProviderMultiSelectProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
selectedProviders,
|
selection,
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
|
|
@ -121,97 +181,100 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowedProviders.length === 0 && !loading) {
|
if (allowedProviders.length === 0 && !loading) {
|
||||||
loadAllowedProviders();
|
loadAllowedProviders();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Apply default exclusions when providers first load
|
// Apply default exclusions once when providers first load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!initialExcludeApplied &&
|
!initialExcludeApplied &&
|
||||||
allowedProviders.length > 0 &&
|
allowedProviders.length > 0 &&
|
||||||
excludeByDefault.length > 0 &&
|
excludeByDefault.length > 0 &&
|
||||||
selectedProviders.length === 0
|
_isAllSelected(selection)
|
||||||
) {
|
) {
|
||||||
const initialSelection = allowedProviders.filter(
|
onChange({ include: [PROVIDER_ALL], exclude: [...excludeByDefault] });
|
||||||
(p) => !excludeByDefault.includes(p)
|
|
||||||
);
|
|
||||||
// Only apply if there's actually something to exclude
|
|
||||||
if (initialSelection.length < allowedProviders.length) {
|
|
||||||
onChange(initialSelection);
|
|
||||||
}
|
|
||||||
setInitialExcludeApplied(true);
|
setInitialExcludeApplied(true);
|
||||||
}
|
}
|
||||||
}, [allowedProviders, excludeByDefault, initialExcludeApplied, selectedProviders.length, onChange]);
|
}, [allowedProviders, excludeByDefault, initialExcludeApplied, selection, onChange]);
|
||||||
|
|
||||||
// Click outside handler
|
const _handleClickOutside = useCallback((event: MouseEvent) => {
|
||||||
const handleClickOutside = useCallback((event: MouseEvent) => {
|
|
||||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', _handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', _handleClickOutside);
|
||||||
}
|
}
|
||||||
}, [isExpanded, handleClickOutside]);
|
}, [isExpanded, _handleClickOutside]);
|
||||||
|
|
||||||
// Effective selection: empty array = all providers active (no restriction)
|
const effectiveSelection = useMemo(
|
||||||
const effectiveSelection = selectedProviders.length === 0 ? allowedProviders : selectedProviders;
|
() => _resolveProviders(selection, allowedProviders),
|
||||||
|
[selection, allowedProviders],
|
||||||
// "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 allSelected = _isAllSelected(selection);
|
||||||
|
const noneSelected = effectiveSelection.length === 0;
|
||||||
const handleToggle = (provider: string) => {
|
|
||||||
if (selectedProviders.length === 0) {
|
const _handleToggle = (provider: string) => {
|
||||||
// Currently "all active" (no restriction) -> make explicit: all except the toggled one
|
const isChecked = effectiveSelection.includes(provider);
|
||||||
onChange(allowedProviders.filter((p) => p !== provider));
|
|
||||||
} else if (selectedProviders.includes(provider)) {
|
if (selection.include.includes(PROVIDER_ALL)) {
|
||||||
// Deactivate: remove from selection
|
// Currently ALL-based: toggle modifies exclude list
|
||||||
const remaining = selectedProviders.filter((p) => p !== provider);
|
if (isChecked) {
|
||||||
// If removing leaves all others selected, reset to [] (= all, no restriction)
|
onChange({ include: [PROVIDER_ALL], exclude: [...selection.exclude, provider] });
|
||||||
if (remaining.length === allowedProviders.length) {
|
|
||||||
onChange([]);
|
|
||||||
} else {
|
} else {
|
||||||
onChange(remaining);
|
const nextExclude = selection.exclude.filter((p) => p !== provider);
|
||||||
|
onChange({ include: [PROVIDER_ALL], exclude: nextExclude });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Activate: add to selection
|
// Explicit include list
|
||||||
const updated = [...selectedProviders, provider];
|
if (isChecked) {
|
||||||
// If all are now selected, reset to [] (= all, no restriction)
|
onChange({ include: selection.include.filter((p) => p !== provider), exclude: [] });
|
||||||
if (updated.length === allowedProviders.length) {
|
|
||||||
onChange([]);
|
|
||||||
} else {
|
} else {
|
||||||
onChange(updated);
|
const nextInclude = [...selection.include, provider];
|
||||||
|
if (nextInclude.length === allowedProviders.length) {
|
||||||
|
onChange({ include: [PROVIDER_ALL], exclude: [] });
|
||||||
|
} else {
|
||||||
|
onChange({ include: nextInclude, exclude: [] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const _handleSelectAll = () => {
|
||||||
onChange([]); // Empty = all active, no restriction
|
onChange({ include: [PROVIDER_ALL], exclude: [] });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Summary icon for button
|
|
||||||
const summaryIcon = useMemo(() => {
|
const summaryIcon = useMemo(() => {
|
||||||
|
if (noneSelected) return '⊘';
|
||||||
if (effectiveSelection.length === 1) {
|
if (effectiveSelection.length === 1) {
|
||||||
return PROVIDER_ICONS[effectiveSelection[0]] || '🔌';
|
return PROVIDER_ICONS[effectiveSelection[0]] || '🔌';
|
||||||
}
|
}
|
||||||
return '🤖';
|
return '⚡';
|
||||||
}, [effectiveSelection]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`${styles.providerMultiSelect} ${className || ''} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
className={`${styles.providerMultiSelect} ${className || ''} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
||||||
>
|
>
|
||||||
{/* Trigger Button - styled like iconButton */}
|
<button
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.triggerButton}
|
className={styles.triggerButton}
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
|
@ -220,36 +283,35 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
>
|
>
|
||||||
<span className={styles.buttonIcon}>{summaryIcon}</span>
|
<span className={styles.buttonIcon}>{summaryIcon}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Content */}
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className={styles.dropdownContent}>
|
<div className={styles.dropdownContent}>
|
||||||
{showLabel && <div className={styles.dropdownHeader}>{label}</div>}
|
{showLabel && <div className={styles.dropdownHeader}>{label}</div>}
|
||||||
|
|
||||||
<div className={styles.selectActions}>
|
<div className={styles.selectActions}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSelectAll}
|
onClick={_handleSelectAll}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`${styles.actionButton} ${isAllSelected ? styles.active : ''}`}
|
className={`${styles.actionButton} ${allSelected ? styles.active : ''}`}
|
||||||
>
|
>
|
||||||
Alle
|
Alle
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={styles.loading}>Lade...</div>
|
<div className={styles.loading}>Lade...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.checkboxList}>
|
<div className={styles.checkboxList}>
|
||||||
{allowedProviders.map((provider) => (
|
{allowedProviders.map((provider) => (
|
||||||
<label
|
<label
|
||||||
key={provider}
|
key={provider}
|
||||||
className={`${styles.checkboxItem} ${disabled ? styles.disabled : ''}`}
|
className={`${styles.checkboxItem} ${disabled ? styles.disabled : ''}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={effectiveSelection.includes(provider)}
|
checked={effectiveSelection.includes(provider)}
|
||||||
onChange={() => handleToggle(provider)}
|
onChange={() => _handleToggle(provider)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
||||||
|
|
@ -260,12 +322,8 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAllSelected && !loading && (
|
<div className={styles.hint}>{summaryHint}</div>
|
||||||
<div className={styles.hint}>
|
|
||||||
Alle Provider aktiv (kein Filter)
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -288,7 +346,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
||||||
if (providers.length === 0) {
|
if (providers.length === 0) {
|
||||||
return <span className={styles.allProviders}>Alle Provider</span>;
|
return <span className={styles.allProviders}>Alle Provider</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.providerBadges} ${className || ''}`}>
|
<div className={`${styles.providerBadges} ${className || ''}`}>
|
||||||
{providers.map((provider) => (
|
{providers.map((provider) => (
|
||||||
|
|
@ -300,5 +358,4 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default export
|
|
||||||
export default ProviderSelect;
|
export default ProviderSelect;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,19 @@
|
||||||
export {
|
export {
|
||||||
ProviderSelect,
|
ProviderSelect,
|
||||||
ProviderMultiSelect,
|
ProviderMultiSelect,
|
||||||
ProviderBadges
|
ProviderBadges,
|
||||||
} from './ProviderSelector';
|
} from './ProviderSelector';
|
||||||
|
|
||||||
|
export {
|
||||||
|
PROVIDER_ALL,
|
||||||
|
_defaultProviderSelection,
|
||||||
|
_resolveProviders,
|
||||||
|
_isAllSelected,
|
||||||
|
_isNoneSelected,
|
||||||
|
_migrateFromLegacy,
|
||||||
|
_toBackendProviders,
|
||||||
|
} from './ProviderSelector';
|
||||||
|
|
||||||
|
export type { ProviderSelection } from './ProviderSelector';
|
||||||
|
|
||||||
export { default } from './ProviderSelector';
|
export { default } from './ProviderSelector';
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export function usePrompt() {
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--surface-color, #1a1a2e)',
|
background: 'var(--surface-color, #1a1a2e)',
|
||||||
border: '1px solid var(--color-border, #333)',
|
border: '1px solid var(--border-color, var(--color-border, #333))',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '1.5rem',
|
padding: '1.5rem',
|
||||||
minWidth: 360, maxWidth: 500,
|
minWidth: 360, maxWidth: 500,
|
||||||
|
|
@ -116,9 +116,9 @@ export function usePrompt() {
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 14px',
|
padding: '10px 14px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid var(--color-border, #444)',
|
border: '1px solid var(--border-color, var(--color-border, #ccc))',
|
||||||
background: 'var(--input-bg, #0d0d1a)',
|
background: 'var(--input-bg, var(--bg-primary, #ffffff))',
|
||||||
color: 'var(--text-primary, #e0e0e0)',
|
color: 'var(--text-primary, #1a1a1a)',
|
||||||
fontSize: '0.9rem',
|
fontSize: '0.9rem',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
||||||
|
|
@ -240,16 +240,9 @@ function Login() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.ctaPrimary}
|
className={styles.ctaPrimary}
|
||||||
onClick={() => navigate('/register?type=personal', { state: location.state })}
|
onClick={() => navigate('/register', { state: location.state })}
|
||||||
>
|
>
|
||||||
Kostenlos testen
|
Kostenlos registrieren
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.ctaSecondary}
|
|
||||||
onClick={() => navigate('/register?type=company', { state: location.state })}
|
|
||||||
>
|
|
||||||
Für Unternehmen
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { FaEnvelopeOpenText } from 'react-icons/fa';
|
import { FaEnvelopeOpenText } from 'react-icons/fa';
|
||||||
|
|
||||||
import styles from './Register.module.css';
|
import styles from './Register.module.css';
|
||||||
|
|
@ -19,7 +19,6 @@ function Register() {
|
||||||
const { register, error: registerError, isLoading } = useRegister();
|
const { register, error: registerError, isLoading } = useRegister();
|
||||||
const { error: msalError } = useMsalRegister();
|
const { error: msalError } = useMsalRegister();
|
||||||
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
|
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
|
||||||
// Pre-fill from invitation if provided via location.state
|
|
||||||
const invitationUsername = (location.state as any)?.invitationUsername || '';
|
const invitationUsername = (location.state as any)?.invitationUsername || '';
|
||||||
const invitationEmail = (location.state as any)?.invitationEmail || '';
|
const invitationEmail = (location.state as any)?.invitationEmail || '';
|
||||||
const [formData, setFormData] = useState<RegisterFormData>({
|
const [formData, setFormData] = useState<RegisterFormData>({
|
||||||
|
|
@ -27,10 +26,6 @@ function Register() {
|
||||||
email: invitationEmail,
|
email: invitationEmail,
|
||||||
fullName: ''
|
fullName: ''
|
||||||
});
|
});
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const registrationType = searchParams.get('type') === 'company' ? 'company' : 'personal';
|
|
||||||
const [companyName, setCompanyName] = useState('');
|
|
||||||
const [companyNameFocused, setCompanyNameFocused] = useState(false);
|
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [usernameFocused, setUsernameFocused] = useState(false);
|
const [usernameFocused, setUsernameFocused] = useState(false);
|
||||||
|
|
@ -38,19 +33,13 @@ function Register() {
|
||||||
const [fullNameFocused, setFullNameFocused] = useState(false);
|
const [fullNameFocused, setFullNameFocused] = useState(false);
|
||||||
const [usernameHighlight, setUsernameHighlight] = useState(false);
|
const [usernameHighlight, setUsernameHighlight] = useState(false);
|
||||||
|
|
||||||
// Check for pending invitation
|
|
||||||
const pendingInvitationToken = localStorage.getItem(PENDING_INVITATION_KEY);
|
const pendingInvitationToken = localStorage.getItem(PENDING_INVITATION_KEY);
|
||||||
const hasPendingInvitation = !!pendingInvitationToken;
|
const hasPendingInvitation = !!pendingInvitationToken;
|
||||||
|
|
||||||
// Set page title and generate CSRF token
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = registrationType === 'company'
|
document.title = "PowerOn AI Platform - Registrieren";
|
||||||
? "PowerOn AI Platform - Unternehmenskonto erstellen"
|
|
||||||
: "PowerOn AI Platform - Kostenlos testen";
|
|
||||||
|
|
||||||
// Generate CSRF token for new security implementation
|
|
||||||
generateAndStoreCSRFToken();
|
generateAndStoreCSRFToken();
|
||||||
}, [registrationType]);
|
}, []);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
@ -59,13 +48,12 @@ function Register() {
|
||||||
[name]: value
|
[name]: value
|
||||||
}));
|
}));
|
||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
// Reset username highlight when user starts typing in username field
|
|
||||||
if (name === 'username') {
|
if (name === 'username') {
|
||||||
setUsernameHighlight(false);
|
setUsernameHighlight(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const _validateForm = (): boolean => {
|
||||||
if (!formData.username || !formData.email || !formData.fullName) {
|
if (!formData.username || !formData.email || !formData.fullName) {
|
||||||
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -76,27 +64,20 @@ function Register() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registrationType === 'company' && !companyName.trim()) {
|
|
||||||
setValidationError('Bitte geben Sie einen Firmennamen ein.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!_validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First check username availability
|
|
||||||
const availabilityResult = await checkAvailability(formData.username, 'local');
|
const availabilityResult = await checkAvailability(formData.username, 'local');
|
||||||
|
|
||||||
if (!availabilityResult.available) {
|
if (!availabilityResult.available) {
|
||||||
// Check if the error message is about username being taken
|
|
||||||
const errorMessage = availabilityResult.message || 'Username is not available';
|
const errorMessage = availabilityResult.message || 'Username is not available';
|
||||||
if (errorMessage === 'Username is already taken') {
|
if (errorMessage === 'Username is already taken') {
|
||||||
setValidationError('Benutzername ist bereits vergeben');
|
setValidationError('Benutzername ist bereits vergeben');
|
||||||
|
|
@ -107,25 +88,20 @@ function Register() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Username is available, proceed with registration (no password - magic link flow)
|
await register({ ...formData, registrationType: 'personal' });
|
||||||
await register({ ...formData, registrationType, companyName: registrationType === 'company' ? companyName : undefined });
|
|
||||||
|
|
||||||
// Build success message
|
|
||||||
let message = 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.';
|
let message = 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.';
|
||||||
if (hasPendingInvitation) {
|
if (hasPendingInvitation) {
|
||||||
message += ' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.';
|
message += ' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message instead of immediate redirect
|
|
||||||
setSuccessMessage(message);
|
setSuccessMessage(message);
|
||||||
|
|
||||||
// Redirect to login page after delay
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: {
|
state: {
|
||||||
registered: true,
|
registered: true,
|
||||||
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.',
|
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.',
|
||||||
// Pass along invitation state
|
|
||||||
...(location.state || {})
|
...(location.state || {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -135,8 +111,7 @@ function Register() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to safely get error message
|
const _getErrorMessage = () => {
|
||||||
const getErrorMessage = () => {
|
|
||||||
if (validationError) return validationError;
|
if (validationError) return validationError;
|
||||||
if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed';
|
if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed';
|
||||||
if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed';
|
if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed';
|
||||||
|
|
@ -157,7 +132,6 @@ function Register() {
|
||||||
<div className={styles.loginSection}>
|
<div className={styles.loginSection}>
|
||||||
<div className={styles.loginBox}>
|
<div className={styles.loginBox}>
|
||||||
<div className={styles.loginForm}>
|
<div className={styles.loginForm}>
|
||||||
{/* Pending invitation notice */}
|
|
||||||
{hasPendingInvitation && !successMessage && (
|
{hasPendingInvitation && !successMessage && (
|
||||||
<div className={styles.invitationNotice}>
|
<div className={styles.invitationNotice}>
|
||||||
<FaEnvelopeOpenText className={styles.invitationIcon} />
|
<FaEnvelopeOpenText className={styles.invitationIcon} />
|
||||||
|
|
@ -165,8 +139,8 @@ function Register() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{getErrorMessage() && (
|
{_getErrorMessage() && (
|
||||||
<div className={styles.error}>{getErrorMessage()}</div>
|
<div className={styles.error}>{_getErrorMessage()}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{successMessage && (
|
{successMessage && (
|
||||||
|
|
@ -203,22 +177,6 @@ function Register() {
|
||||||
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label>
|
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{registrationType === 'company' && (
|
|
||||||
<div className={styles.floatingLabelInput}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="companyName"
|
|
||||||
placeholder=" "
|
|
||||||
value={companyName}
|
|
||||||
onChange={(e) => setCompanyName(e.target.value)}
|
|
||||||
onFocus={() => setCompanyNameFocused(true)}
|
|
||||||
onBlur={() => setCompanyNameFocused(false)}
|
|
||||||
className={`${styles.input} ${companyNameFocused || companyName ? styles.focused : ''}`}
|
|
||||||
/>
|
|
||||||
<label className={companyNameFocused || companyName ? styles.focusedLabel : styles.label}>Firmenname *</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.floatingLabelInput}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -248,7 +206,7 @@ function Register() {
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isLoading || isChecking}
|
disabled={isLoading || isChecking}
|
||||||
>
|
>
|
||||||
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : registrationType === 'company' ? 'Unternehmenskonto erstellen' : 'Kostenlos testen'}
|
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : 'Kostenlos registrieren'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
||||||
if (billingSaved) {
|
if (billingSaved) {
|
||||||
showSuccess('Erstellt', 'Mandant inkl. Abrechnung gespeichert');
|
showSuccess('Erstellt', 'Mandant inkl. Abrechnung gespeichert');
|
||||||
}
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||||
await loadMandates();
|
await loadMandates();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const e = err as { response?: { data?: { detail?: string } }; message?: string };
|
const e = err as { response?: { data?: { detail?: string } }; message?: string };
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { ProviderMultiSelect } from '../../../components/ProviderSelector';
|
import { ProviderMultiSelect } from '../../../components/ProviderSelector';
|
||||||
|
import type { ProviderSelection } from '../../../components/ProviderSelector';
|
||||||
import { getPageIcon } from '../../../config/pageRegistry';
|
import { getPageIcon } from '../../../config/pageRegistry';
|
||||||
import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture';
|
import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture';
|
||||||
import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace';
|
import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace';
|
||||||
|
|
@ -48,8 +49,8 @@ interface WorkspaceInputProps {
|
||||||
onRemovePendingFile?: (fileId: string) => void;
|
onRemovePendingFile?: (fileId: string) => void;
|
||||||
onFileUploadClick?: () => void;
|
onFileUploadClick?: () => void;
|
||||||
uploading?: boolean;
|
uploading?: boolean;
|
||||||
selectedProviders?: string[];
|
providerSelection?: ProviderSelection;
|
||||||
onProvidersChange?: (providers: string[]) => void;
|
onProviderSelectionChange?: (selection: ProviderSelection) => void;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
onTreeItemsDrop?: (items: TreeItemDrop[]) => void;
|
onTreeItemsDrop?: (items: TreeItemDrop[]) => void;
|
||||||
onPasteAsFile?: (file: File) => void;
|
onPasteAsFile?: (file: File) => void;
|
||||||
|
|
@ -69,8 +70,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
||||||
onRemovePendingFile,
|
onRemovePendingFile,
|
||||||
onFileUploadClick,
|
onFileUploadClick,
|
||||||
uploading = false,
|
uploading = false,
|
||||||
selectedProviders = [],
|
providerSelection,
|
||||||
onProvidersChange,
|
onProviderSelectionChange,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
onTreeItemsDrop,
|
onTreeItemsDrop,
|
||||||
onPasteAsFile,
|
onPasteAsFile,
|
||||||
|
|
@ -653,12 +654,11 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onProvidersChange && (
|
{onProviderSelectionChange && providerSelection && (
|
||||||
<ProviderMultiSelect
|
<ProviderMultiSelect
|
||||||
selectedProviders={selectedProviders}
|
selection={providerSelection}
|
||||||
onChange={onProvidersChange}
|
onChange={onProviderSelectionChange}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
excludeByDefault={['privatellm']}
|
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ import { ToolActivityLog } from './ToolActivityLog';
|
||||||
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
||||||
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
import { _defaultProviderSelection, _toBackendProviders } from '../../../components/ProviderSelector';
|
||||||
|
import type { ProviderSelection } from '../../../components/ProviderSelector';
|
||||||
|
import { useBilling } from '../../../hooks/useBilling';
|
||||||
|
|
||||||
function _useResizable(initialWidth: number, minWidth: number, maxWidth: number) {
|
function _useResizable(initialWidth: number, minWidth: number, maxWidth: number) {
|
||||||
const [width, setWidth] = useState(initialWidth);
|
const [width, setWidth] = useState(initialWidth);
|
||||||
|
|
@ -81,7 +84,8 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
const [udbTab, setUdbTab] = useState<UdbTab>('chats');
|
const [udbTab, setUdbTab] = useState<UdbTab>('chats');
|
||||||
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
|
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
|
||||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||||
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
|
const [providerSelection, setProviderSelection] = useState<ProviderSelection>(_defaultProviderSelection());
|
||||||
|
const { allowedProviders } = useBilling();
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [draftAppend, setDraftAppend] = useState('');
|
const [draftAppend, setDraftAppend] = useState('');
|
||||||
const dragCounterRef = useRef(0);
|
const dragCounterRef = useRef(0);
|
||||||
|
|
@ -414,7 +418,8 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
onSend={(prompt, fileIds, dataSourceIds, featureDataSourceIds, options) => {
|
onSend={(prompt, fileIds, dataSourceIds, featureDataSourceIds, options) => {
|
||||||
const allFileIds = [...new Set([...pendingFiles.map(f => f.fileId), ...(fileIds || [])])];
|
const allFileIds = [...new Set([...pendingFiles.map(f => f.fileId), ...(fileIds || [])])];
|
||||||
workspace.sendMessage(prompt, allFileIds, dataSourceIds, selectedProviders, featureDataSourceIds, options);
|
const resolvedProviders = _toBackendProviders(providerSelection, allowedProviders);
|
||||||
|
workspace.sendMessage(prompt, allFileIds, dataSourceIds, resolvedProviders, featureDataSourceIds, options);
|
||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
}}
|
}}
|
||||||
isProcessing={workspace.isProcessing}
|
isProcessing={workspace.isProcessing}
|
||||||
|
|
@ -426,8 +431,8 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
onRemovePendingFile={_handleRemovePendingFile}
|
onRemovePendingFile={_handleRemovePendingFile}
|
||||||
onFileUploadClick={() => fileInputRef.current?.click()}
|
onFileUploadClick={() => fileInputRef.current?.click()}
|
||||||
uploading={fileOps.uploadingFile}
|
uploading={fileOps.uploadingFile}
|
||||||
selectedProviders={selectedProviders}
|
providerSelection={providerSelection}
|
||||||
onProvidersChange={setSelectedProviders}
|
onProviderSelectionChange={setProviderSelection}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onTreeItemsDrop={_handleTreeItemsDrop}
|
onTreeItemsDrop={_handleTreeItemsDrop}
|
||||||
onPasteAsFile={_uploadAndAttach}
|
onPasteAsFile={_uploadAndAttach}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue