ui-nyla/src/components/OnboardingAssistant.tsx
2026-03-28 16:58:55 +01:00

294 lines
10 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import api from '../api';
interface OnboardingStep {
id: string;
label: string;
description: string;
completed: boolean;
action?: () => void;
}
interface OnboardingAssistantProps {
onDismiss?: () => void;
}
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.',
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.',
};
export function _isOnboardingHidden(): boolean {
try {
return localStorage.getItem(_STORAGE_KEY) === 'true';
} catch {
return false;
}
}
export function _showOnboarding(): void {
try {
localStorage.removeItem(_STORAGE_KEY);
} catch { /* ignore */ }
}
function _hideOnboarding(): void {
try {
localStorage.setItem(_STORAGE_KEY, 'true');
} catch { /* ignore */ }
}
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
const navigate = useNavigate();
const location = useLocation();
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
const [steps, setSteps] = useState<OnboardingStep[]>([]);
const [loading, setLoading] = useState(true);
const [dontShowAgain, setDontShowAgain] = useState(false);
const _checkOnboardingState = useCallback(async () => {
setLoading(true);
try {
const onboardingSteps: OnboardingStep[] = [];
let hasMandate = false;
try {
const mandatesRes = await api.get('/api/store/mandates');
const mandates = mandatesRes.data?.mandates || mandatesRes.data || [];
hasMandate = 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'),
});
let hasFeature = false;
let firstInstancePath: string | undefined;
try {
const navRes = await api.get('/api/navigation?language=de');
const mandates = navRes.data?.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;
}
}
}
}
} catch { /* ignore */ }
onboardingSteps.push({
id: 'feature',
label: 'Erstes Feature aktivieren',
description: hasFeature
? 'Du hast aktive Features.'
: 'Aktiviere dein erstes Feature im Store.',
completed: hasFeature,
action: hasFeature ? undefined : () => navigate('/store'),
});
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;
} catch { /* ignore */ }
onboardingSteps.push({
id: 'connection',
label: 'Erste Datenquelle einbinden',
description: hasConnection
? 'Du hast Verbindungen eingerichtet.'
: 'Verbinde deine erste Datenquelle.',
completed: hasConnection,
action: hasConnection ? undefined : () => navigate('/basedata/connections'),
});
let hasChat = false;
if (hasFeature && firstInstancePath) {
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 */ }
}
}
} catch { /* ignore */ }
}
const _chatAction = firstInstancePath ? () => navigate(firstInstancePath!) : undefined;
onboardingSteps.push({
id: 'chat',
label: 'Ersten AI-Chat starten',
description: hasChat
? 'Du hast bereits Chats gestartet.'
: 'Starte deinen ersten Chat mit dem AI-Assistenten.',
completed: hasChat,
action: hasChat ? undefined : _chatAction,
});
setSteps(onboardingSteps);
if (onboardingSteps.every(s => s.completed)) {
setHidden(true);
_hideOnboarding();
}
} catch (err) {
console.error('Onboarding check failed:', err);
} finally {
setLoading(false);
}
}, [navigate]);
useEffect(() => {
const state = location.state as { showOnboarding?: number } | null;
if (state?.showOnboarding) {
setHidden(false);
window.history.replaceState({}, '');
}
}, [location.state]);
useEffect(() => {
if (!hidden) _checkOnboardingState();
}, [hidden, _checkOnboardingState]);
const _handleDismiss = () => {
if (dontShowAgain) {
_hideOnboarding();
}
setHidden(true);
onDismiss?.();
};
if (hidden || loading) return null;
const completedCount = steps.filter(s => s.completed).length;
if (completedCount === steps.length) return null;
return (
<div style={{
padding: 16, margin: '0 0 20px 0', borderRadius: 12,
border: '1px solid var(--border-color, #e5e7eb)',
background: 'linear-gradient(135deg, var(--bg-primary, #fff) 0%, #eef2ff 100%)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
<div>
<h3 style={{ margin: 0, fontSize: '1rem' }}>Willkommen bei PowerOn</h3>
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
{completedCount} von {steps.length} Schritten abgeschlossen
</p>
</div>
</div>
<div style={{ display: 'flex', gap: 4, marginBottom: 16, height: 4 }}>
{steps.map((step) => (
<div
key={step.id}
style={{
flex: 1, borderRadius: 2, height: 4,
background: step.completed ? 'var(--accent, #4f46e5)' : 'var(--border-color, #e5e7eb)',
transition: 'background 0.3s',
}}
/>
))}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{steps.map((step, idx) => {
const isNextStep = !step.completed && steps.slice(0, idx).every(s => s.completed);
return (
<div key={step.id}>
<div
style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
borderRadius: 8, background: step.completed ? 'transparent' : 'var(--bg-primary, #fff)',
border: step.completed ? 'none' : isNextStep ? '1px solid var(--accent, #4f46e5)' : '1px solid var(--border-color, #e5e7eb)',
opacity: step.completed ? 0.6 : 1,
cursor: step.action ? 'pointer' : 'default',
}}
onClick={step.action}
>
<span style={{ fontSize: '1.1rem', flexShrink: 0, width: 24, textAlign: 'center' }}>
{step.completed ? '\u2713' : '\u25CB'}
</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500, fontSize: '0.85rem', textDecoration: step.completed ? 'line-through' : 'none' }}>
{step.label}
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #6b7280)' }}>
{step.description}
</div>
</div>
{step.action && !step.completed && (
<span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
)}
</div>
{isNextStep && _CALLOUTS[step.id] && (
<div style={{
marginTop: 4, marginLeft: 34, padding: '6px 10px',
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
borderLeft: '3px solid var(--accent, #4f46e5)',
}}>
{_CALLOUTS[step.id]}
</div>
)}
</div>
);
})}
</div>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginTop: 14, paddingTop: 10,
borderTop: '1px solid var(--border-color, #e5e7eb)',
}}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '0.8rem', color: 'var(--text-secondary, #6b7280)', cursor: 'pointer' }}>
<input
type="checkbox"
checked={dontShowAgain}
onChange={(e) => setDontShowAgain(e.target.checked)}
style={{ margin: 0 }}
/>
Nicht wieder anzeigen
</label>
<button
onClick={_handleDismiss}
style={{
border: '1px solid var(--border-color, #d1d5db)',
background: 'transparent',
cursor: 'pointer',
fontSize: '0.8rem',
color: 'var(--text-secondary, #6b7280)',
padding: '4px 12px',
borderRadius: 6,
}}
>
Schliessen
</button>
</div>
</div>
);
};
export default OnboardingAssistant;