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