354 lines
14 KiB
TypeScript
354 lines
14 KiB
TypeScript
/**
|
|
* TeamsBot Assistant View
|
|
*
|
|
* Wizard: Select/create module → Meeting link → Bot selection → "Start bot"
|
|
*/
|
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
|
import * as teamsbotApi from '../../../api/teamsbotApi';
|
|
import type { MeetingModule, TeamsbotJoinMode, UserAccountStatus } from '../../../api/teamsbotApi';
|
|
import { getUserDataCache } from '../../../utils/userCache';
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
import styles from './Teamsbot.module.css';
|
|
|
|
type WizardStep = 'module' | 'meeting' | 'bot' | 'confirm';
|
|
const STEPS: WizardStep[] = ['module', 'meeting', 'bot', 'confirm'];
|
|
|
|
export const TeamsbotAssistantView: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const { instance, mandateId } = useCurrentInstance();
|
|
const instanceId = instance?.id || '';
|
|
const navigate = useNavigate();
|
|
const cachedUser = getUserDataCache();
|
|
const isSysAdmin = cachedUser?.isSysAdmin === true;
|
|
const [searchParams] = useSearchParams();
|
|
const preselectedModuleId = searchParams.get('moduleId');
|
|
|
|
const [step, setStep] = useState<WizardStep>(preselectedModuleId ? 'meeting' : 'module');
|
|
const [modules, setModules] = useState<MeetingModule[]>([]);
|
|
const [moduleFilter, setModuleFilter] = useState('');
|
|
const [selectedModuleId, setSelectedModuleId] = useState<string | null>(preselectedModuleId);
|
|
const [newModuleTitle, setNewModuleTitle] = useState('');
|
|
const [createNewModule, setCreateNewModule] = useState(false);
|
|
const [meetingLink, setMeetingLink] = useState('');
|
|
const [botName, setBotName] = useState('AI Assistant');
|
|
const [joinMode, setJoinMode] = useState<TeamsbotJoinMode>('anonymous');
|
|
const [sessionContext, setSessionContext] = useState('');
|
|
const [userAccount, setUserAccount] = useState<UserAccountStatus | null>(null);
|
|
const [showCredentialForm, setShowCredentialForm] = useState(false);
|
|
const [credEmail, setCredEmail] = useState('');
|
|
const [credPassword, setCredPassword] = useState('');
|
|
const [savingCredentials, setSavingCredentials] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const stepIdx = STEPS.indexOf(step);
|
|
|
|
const _loadModules = useCallback(async () => {
|
|
if (!instanceId) return;
|
|
try {
|
|
const result = await teamsbotApi.listModules(instanceId);
|
|
setModules(result || []);
|
|
} catch (err) {
|
|
console.error('Failed to load modules:', err);
|
|
}
|
|
}, [instanceId]);
|
|
|
|
useEffect(() => { _loadModules(); }, [_loadModules]);
|
|
|
|
useEffect(() => {
|
|
if (joinMode === 'userAccount' && instanceId) {
|
|
teamsbotApi.getUserAccount(instanceId).then(setUserAccount).catch(() => setUserAccount(null));
|
|
}
|
|
}, [joinMode, instanceId]);
|
|
|
|
const filteredModules = useMemo(() => {
|
|
const q = moduleFilter.trim().toLowerCase();
|
|
if (!q) return modules;
|
|
return modules.filter(m => m.title.toLowerCase().includes(q));
|
|
}, [modules, moduleFilter]);
|
|
|
|
const modulePrefillKeyRef = useRef<string>('');
|
|
useEffect(() => {
|
|
if (!selectedModuleId || createNewModule) {
|
|
modulePrefillKeyRef.current = '';
|
|
return;
|
|
}
|
|
const mod = modules.find(m => m.id === selectedModuleId);
|
|
if (!mod) return;
|
|
const key = `${selectedModuleId}:${mod.defaultMeetingLink ?? ''}:${mod.defaultBotName ?? ''}`;
|
|
if (modulePrefillKeyRef.current === key) return;
|
|
modulePrefillKeyRef.current = key;
|
|
if (mod.defaultMeetingLink) setMeetingLink(mod.defaultMeetingLink);
|
|
if (mod.defaultBotName) setBotName(mod.defaultBotName);
|
|
}, [selectedModuleId, createNewModule, modules]);
|
|
|
|
const _handleNext = () => {
|
|
const nextIdx = stepIdx + 1;
|
|
if (nextIdx < STEPS.length) setStep(STEPS[nextIdx]);
|
|
};
|
|
|
|
const _handleBack = () => {
|
|
const prevIdx = stepIdx - 1;
|
|
if (prevIdx >= 0) setStep(STEPS[prevIdx]);
|
|
};
|
|
|
|
const _handleStart = async () => {
|
|
if (!meetingLink.trim()) {
|
|
setError(t('Meeting-Link erforderlich'));
|
|
return;
|
|
}
|
|
if (joinMode === 'userAccount' && !userAccount?.hasSavedCredentials && !credEmail) {
|
|
setShowCredentialForm(true);
|
|
setError(t('Bitte Microsoft-Zugangsdaten eingeben oder speichern.'));
|
|
return;
|
|
}
|
|
const needsSave = joinMode === 'userAccount' && !userAccount?.hasSavedCredentials && credEmail && credPassword;
|
|
const needsUpdate = joinMode === 'userAccount' && showCredentialForm && credEmail && credPassword;
|
|
if (needsSave || needsUpdate) {
|
|
try {
|
|
setSavingCredentials(true);
|
|
await teamsbotApi.saveUserAccount(instanceId, credEmail, credPassword);
|
|
setUserAccount({ hasSavedCredentials: true, email: credEmail });
|
|
setShowCredentialForm(false);
|
|
} catch (err: any) {
|
|
setError(err?.message || t('Fehler beim Speichern der Zugangsdaten'));
|
|
setSavingCredentials(false);
|
|
return;
|
|
} finally {
|
|
setSavingCredentials(false);
|
|
}
|
|
}
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
let moduleId = selectedModuleId;
|
|
if (createNewModule && newModuleTitle.trim()) {
|
|
const mod = await teamsbotApi.createModule(instanceId, { title: newModuleTitle.trim() });
|
|
moduleId = mod.id;
|
|
}
|
|
|
|
const result = await teamsbotApi.startSession(instanceId, {
|
|
meetingLink: meetingLink.trim(),
|
|
botName,
|
|
moduleId: moduleId || undefined,
|
|
joinMode,
|
|
sessionContext: sessionContext.trim() || undefined,
|
|
});
|
|
|
|
navigate(`/mandates/${mandateId}/teamsbot/${instanceId}/sessions?sessionId=${result.session.id}`);
|
|
} catch (err: any) {
|
|
setError(err?.message || t('Fehler beim Starten'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.assistantContainer}>
|
|
<div className={styles.wizardHeader}>
|
|
<h2>{t('Neues Meeting starten')}</h2>
|
|
<div className={styles.stepIndicator}>
|
|
{STEPS.map((s, i) => (
|
|
<div key={s} className={`${styles.stepDot} ${i <= stepIdx ? styles.stepActive : ''}`} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{error && <div className={styles.errorBanner}>{error}</div>}
|
|
|
|
<div className={styles.wizardContent}>
|
|
{step === 'module' && (
|
|
<div className={styles.wizardStep}>
|
|
<h3>{t('Meeting-Modul wählen')}</h3>
|
|
<div className={styles.moduleChoice}>
|
|
<label>
|
|
<input type="radio" checked={!createNewModule} onChange={() => setCreateNewModule(false)} />
|
|
{t('Bestehendes Modul')}
|
|
</label>
|
|
{!createNewModule && (
|
|
<>
|
|
<input
|
|
type="search"
|
|
className={styles.wizardInput}
|
|
placeholder={t('Modul suchen…')}
|
|
value={moduleFilter}
|
|
onChange={e => setModuleFilter(e.target.value)}
|
|
aria-label={t('Modul suchen')}
|
|
/>
|
|
<select
|
|
value={selectedModuleId || ''}
|
|
onChange={e => setSelectedModuleId(e.target.value || null)}
|
|
className={styles.wizardSelect}
|
|
size={Math.min(12, Math.max(4, filteredModules.length + 1))}
|
|
>
|
|
<option value="">{t('Kein Modul (Adhoc)')}</option>
|
|
{filteredModules.map(m => (
|
|
<option key={m.id} value={m.id}>{m.title}</option>
|
|
))}
|
|
</select>
|
|
</>
|
|
)}
|
|
<label>
|
|
<input type="radio" checked={createNewModule} onChange={() => setCreateNewModule(true)} />
|
|
{t('Neues Modul erstellen')}
|
|
</label>
|
|
{createNewModule && (
|
|
<input
|
|
type="text"
|
|
className={styles.wizardInput}
|
|
placeholder={t('z.B. Weekly Standup, Q3 Review...')}
|
|
value={newModuleTitle}
|
|
onChange={e => setNewModuleTitle(e.target.value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'meeting' && (
|
|
<div className={styles.wizardStep}>
|
|
<h3>{t('Meeting-Link und Beitritt')}</h3>
|
|
<input
|
|
type="text"
|
|
className={styles.wizardInput}
|
|
placeholder="https://teams.microsoft.com/l/meetup-join/..."
|
|
value={meetingLink}
|
|
onChange={e => setMeetingLink(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
<label className={styles.label} style={{ marginTop: '1rem' }}>{t('Join-Modus')}</label>
|
|
<select
|
|
className={styles.wizardSelect}
|
|
value={joinMode}
|
|
onChange={e => setJoinMode(e.target.value as TeamsbotJoinMode)}
|
|
>
|
|
{isSysAdmin && <option value="systemBot">{t('Systembot authentifiziert')}</option>}
|
|
<option value="anonymous">{t('Anonymer Gast')}</option>
|
|
<option value="userAccount">{t('Mein Account')}</option>
|
|
</select>
|
|
{joinMode === 'userAccount' && (
|
|
<div className={styles.credentialsCard} style={{ marginTop: '0.75rem' }}>
|
|
{userAccount?.hasSavedCredentials && !showCredentialForm ? (
|
|
<div className={styles.credentialsInfo}>
|
|
<span>
|
|
{t('Gespeichert:')} <span className={styles.credentialsEmail}>{userAccount.email}</span>
|
|
</span>
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
<button
|
|
type="button"
|
|
className={styles.viewButton}
|
|
onClick={() => { setShowCredentialForm(true); setCredEmail(userAccount.email || ''); }}
|
|
>
|
|
{t('Ändern')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={styles.deleteButton}
|
|
onClick={async () => {
|
|
try {
|
|
await teamsbotApi.deleteUserAccount(instanceId);
|
|
setUserAccount({ hasSavedCredentials: false });
|
|
setCredEmail('');
|
|
setCredPassword('');
|
|
} catch { /* ignore */ }
|
|
}}
|
|
>
|
|
{t('Entfernen')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>{t('Microsoft E-Mail')}</label>
|
|
<input
|
|
type="email"
|
|
className={styles.input}
|
|
value={credEmail}
|
|
onChange={e => setCredEmail(e.target.value)}
|
|
disabled={savingCredentials}
|
|
/>
|
|
</div>
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>{t('Passwort')}</label>
|
|
<input
|
|
type="password"
|
|
className={styles.input}
|
|
value={credPassword}
|
|
onChange={e => setCredPassword(e.target.value)}
|
|
disabled={savingCredentials}
|
|
/>
|
|
</div>
|
|
<span className={styles.hint}>{t('Zugangsdaten werden verschlüsselt gespeichert.')}</span>
|
|
{userAccount?.hasSavedCredentials && (
|
|
<button type="button" className={styles.viewButton} style={{ marginTop: '0.5rem' }} onClick={() => setShowCredentialForm(false)}>
|
|
{t('Abbrechen')}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
<label className={styles.label} style={{ marginTop: '1rem' }}>{t('Sitzungskontext (optional)')}</label>
|
|
<textarea
|
|
className={styles.wizardTextarea}
|
|
placeholder={t('Agenda, Hintergrund …')}
|
|
value={sessionContext}
|
|
onChange={e => setSessionContext(e.target.value)}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'bot' && (
|
|
<div className={styles.wizardStep}>
|
|
<h3>{t('Bot-Name')}</h3>
|
|
<input
|
|
type="text"
|
|
className={styles.wizardInput}
|
|
value={botName}
|
|
onChange={e => setBotName(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'confirm' && (
|
|
<div className={styles.wizardStep}>
|
|
<h3>{t('Zusammenfassung')}</h3>
|
|
<div className={styles.confirmSummary}>
|
|
<div><strong>{t('Modul')}:</strong> {createNewModule ? newModuleTitle : (modules.find(m => m.id === selectedModuleId)?.title || t('Adhoc'))}</div>
|
|
<div><strong>{t('Meeting')}:</strong> {meetingLink}</div>
|
|
<div><strong>{t('Bot')}:</strong> {botName}</div>
|
|
<div><strong>{t('Join-Modus')}:</strong> {joinMode}</div>
|
|
{sessionContext.trim() ? <div><strong>{t('Kontext')}:</strong> {sessionContext.trim().slice(0, 120)}{sessionContext.length > 120 ? '…' : ''}</div> : null}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.wizardActions}>
|
|
{stepIdx > 0 && (
|
|
<button className={styles.btnSecondary} onClick={_handleBack}>{t('Zurück')}</button>
|
|
)}
|
|
<div style={{ flex: 1 }} />
|
|
{step !== 'confirm' ? (
|
|
<button className={styles.btnPrimary} onClick={_handleNext}>{t('Weiter')}</button>
|
|
) : (
|
|
<button
|
|
className={styles.btnPrimary}
|
|
onClick={_handleStart}
|
|
disabled={
|
|
loading
|
|
|| savingCredentials
|
|
|| (joinMode === 'userAccount' && showCredentialForm && (!credEmail || !credPassword))
|
|
}
|
|
>
|
|
{loading ? t('Starte...') : t('Bot starten')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|