From 544f36460a67bc1aea433d9ad1fa78dd5a045a96 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 11 May 2026 21:26:24 +0200 Subject: [PATCH 01/10] google keys transferred to account poweron.center.ai --- public/poweron-home.html | 5 +- public/poweron-privacy.html | 13 +- public/poweron-terms.html | 13 +- src/api/teamsbotApi.ts | 11 + src/config/pageRegistry.tsx | 2 + src/hooks/useSpeechAudioCapture.ts | 34 +- .../views/commcoach/useVoiceController.ts | 11 +- src/pages/views/teamsbot/Teamsbot.module.css | 223 ++++++ .../views/teamsbot/TeamsbotAssistantView.tsx | 194 ++++- .../views/teamsbot/TeamsbotDashboardView.tsx | 708 +++++++----------- .../views/teamsbot/TeamsbotModulesView.tsx | 182 ++++- .../views/teamsbot/TeamsbotSessionView.tsx | 138 +++- src/types/mandate.ts | 2 +- 13 files changed, 1050 insertions(+), 486 deletions(-) diff --git a/public/poweron-home.html b/public/poweron-home.html index 0a3c92a..2fb440a 100644 --- a/public/poweron-home.html +++ b/public/poweron-home.html @@ -185,7 +185,10 @@ diff --git a/public/poweron-privacy.html b/public/poweron-privacy.html index 1045b30..76ef25c 100644 --- a/public/poweron-privacy.html +++ b/public/poweron-privacy.html @@ -140,7 +140,7 @@
- Last Updated: August 2025 + Last Updated: May 2026
@@ -272,8 +272,13 @@

Contact Us

If you have any questions about this Privacy Policy or our data practices, please contact us:

-

Email: privacy@poweron-ai.com

-

Address: PowerOn AI Platform, Privacy Team

+

Email: p.motsch@poweron.swiss

+

Address:
+ PowerOn AG
+ Birmensdorferstrasse 94
+ CH-8003 Zürich
+ Switzerland +

@@ -283,7 +288,7 @@ diff --git a/public/poweron-terms.html b/public/poweron-terms.html index c9e057d..c049dbe 100644 --- a/public/poweron-terms.html +++ b/public/poweron-terms.html @@ -153,7 +153,7 @@
- Last Updated: August 2025 + Last Updated: May 2026
@@ -315,8 +315,13 @@

Contact Information

If you have any questions about these Terms of Service, please contact us:

-

Email: legal@poweron-ai.com

-

Address: PowerOn AI Platform, Legal Department

+

Email: p.motsch@poweron.swiss

+

Address:
+ PowerOn AG
+ Birmensdorferstrasse 94
+ CH-8003 Zürich
+ Switzerland +

@@ -326,7 +331,7 @@ diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index 3918f7c..8f201c6 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -84,6 +84,7 @@ export interface TeamsbotSessionStats { export interface StartSessionRequest { meetingLink: string; botName?: string; + moduleId?: string; connectionId?: string; joinMode?: TeamsbotJoinMode; sessionContext?: string; @@ -462,6 +463,13 @@ export function createSessionStream(instanceId: string, sessionId: string): Even return new EventSource(url, { withCredentials: true }); } +/** SSE dashboard stream: periodic { type: 'dashboardState', sessions, modules } */ +export function createDashboardStream(instanceId: string): EventSource { + const baseUrl = api.defaults.baseURL || ''; + const url = `${baseUrl}/api/teamsbot/${instanceId}/dashboard/stream`; + return new EventSource(url, { withCredentials: true }); +} + // ========================================================================= // Debug Screenshots (SysAdmin only) // ========================================================================= @@ -592,6 +600,8 @@ export interface MeetingModule { defaultDirectorPrompts?: string; goals?: string; kpiTargets?: string; + defaultMeetingLink?: string; + defaultBotName?: string; status: string; } @@ -602,6 +612,7 @@ export async function listModules(instanceId: string): Promise export async function createModule(instanceId: string, body: { title: string; seriesType?: string; defaultBotId?: string; goals?: string; kpiTargets?: string; + defaultMeetingLink?: string; defaultBotName?: string; }): Promise { const response = await api.post(`/api/teamsbot/${instanceId}/modules`, body); return response.data?.module; diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index d8b84c1..ea22f44 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -109,6 +109,8 @@ export const PAGE_ICONS: Record = { // Feature pages - Teams Bot 'page.feature.teamsbot.dashboard': , + 'page.feature.teamsbot.assistant': , + 'page.feature.teamsbot.modules': , 'page.feature.teamsbot.sessions': , 'page.feature.teamsbot.settings': , diff --git a/src/hooks/useSpeechAudioCapture.ts b/src/hooks/useSpeechAudioCapture.ts index 1b6d7ac..6645289 100644 --- a/src/hooks/useSpeechAudioCapture.ts +++ b/src/hooks/useSpeechAudioCapture.ts @@ -21,10 +21,17 @@ export interface VoiceStreamCallbacks { onError?: (error: unknown) => void; } +/** Options for the initial `open` message on the generic STT WebSocket (Google streaming). */ +export interface SttStreamOpenOptions { + model?: string; + lightweight?: boolean; + singleUtterance?: boolean; +} + export interface VoiceStreamApi { status: VoiceStreamStatus; interimText: string; - start: (language?: string) => Promise; + start: (language?: string, sttOpenOptions?: SttStreamOpenOptions) => Promise; stop: () => void; } @@ -42,6 +49,7 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi const recorderRef = useRef(null); const streamRef = useRef(null); const languageRef = useRef('de-DE'); + const sttOpenOptsRef = useRef(undefined); const stoppingRef = useRef(false); const reconnectAttemptsRef = useRef(0); @@ -94,11 +102,23 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi stoppingRef.current = false; }, [_stopRecorder, _closeWs, _releaseDevices, _setStatus]); - const start = useCallback(async (language?: string) => { + const _buildOpenPayload = useCallback(() => { + const o = sttOpenOptsRef.current; + return { + type: 'open' as const, + language: languageRef.current, + model: o?.model ?? 'latest_long', + lightweight: o?.lightweight ?? false, + singleUtterance: o?.singleUtterance ?? false, + }; + }, []); + + const start = useCallback(async (language?: string, sttOpenOptions?: SttStreamOpenOptions) => { if (status === 'listening' || status === 'connecting') return; stoppingRef.current = false; reconnectAttemptsRef.current = 0; languageRef.current = language || 'de-DE'; + sttOpenOptsRef.current = sttOpenOptions; _setStatus('connecting'); try { @@ -120,7 +140,7 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi ws.onopen = () => { if (stoppingRef.current) { ws.close(); return; } - ws.send(JSON.stringify({ type: 'open', language: languageRef.current })); + ws.send(JSON.stringify(_buildOpenPayload())); const mimeType = _pickMimeType(); const recorder = new MediaRecorder(streamRef.current!, { mimeType }); @@ -154,11 +174,15 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi cbRef.current.onFinal?.(msg.text); } else if (msg.type === 'error') { cbRef.current.onError?.(new Error(msg.message || msg.code || 'STT error')); + } else if (msg.type === 'end_of_single_utterance') { + if (!stoppingRef.current && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(_buildOpenPayload())); + } } else if (msg.type === 'reconnect_required') { if (reconnectAttemptsRef.current < _MAX_RECONNECT_ATTEMPTS && !stoppingRef.current) { reconnectAttemptsRef.current++; _closeWs(); - start(languageRef.current).catch(() => {}); + start(languageRef.current, sttOpenOptsRef.current).catch(() => {}); } } } catch { /* ignore parse errors */ } @@ -183,7 +207,7 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi _releaseDevices(); throw err; } - }, [status, _setStatus, _pickMimeType, _closeWs, _releaseDevices]); + }, [status, _setStatus, _pickMimeType, _closeWs, _releaseDevices, _buildOpenPayload]); useEffect(() => { return () => { diff --git a/src/pages/views/commcoach/useVoiceController.ts b/src/pages/views/commcoach/useVoiceController.ts index 148bbed..7a2ad29 100644 --- a/src/pages/views/commcoach/useVoiceController.ts +++ b/src/pages/views/commcoach/useVoiceController.ts @@ -10,7 +10,7 @@ */ import { useState, useRef, useCallback, useEffect } from 'react'; -import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture'; +import { useVoiceStream, type SttStreamOpenOptions } from '../../../hooks/useSpeechAudioCapture'; import api from '../../../api'; export type VoiceState = 'idle' | 'listening' | 'botSpeaking' | 'interrupted'; @@ -35,6 +35,13 @@ export interface VoiceControllerCallbacks { const _DEFAULT_STT_LANGUAGE = 'de-DE'; +/** CommCoach: faster streaming STT profile + single-utterance endpointing (client re-opens stream). */ +const _commcoachSttOpen: SttStreamOpenOptions = { + model: 'latest_short', + lightweight: true, + singleUtterance: true, +}; + export function useVoiceController(callbacks: VoiceControllerCallbacks): VoiceControllerApi { const [state, setState] = useState('idle'); const [muted, setMuted] = useState(false); @@ -86,7 +93,7 @@ export function useVoiceController(callbacks: VoiceControllerCallbacks): VoiceCo }); const _startStream = useCallback(() => { - return voiceStream.start(sttLanguageRef.current); + return voiceStream.start(sttLanguageRef.current, _commcoachSttOpen); }, [voiceStream]); const activate = useCallback(async () => { diff --git a/src/pages/views/teamsbot/Teamsbot.module.css b/src/pages/views/teamsbot/Teamsbot.module.css index 7d4ae26..4ca21b2 100644 --- a/src/pages/views/teamsbot/Teamsbot.module.css +++ b/src/pages/views/teamsbot/Teamsbot.module.css @@ -1478,6 +1478,10 @@ border-color: var(--primary-color, #4A90D9); } +.moduleRowFocused { + box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.45); +} + .moduleRow { display: flex; align-items: center; @@ -1654,3 +1658,222 @@ padding: 2rem; color: var(--text-secondary, #666); } + +/* --- TeamsBot Dashboard (Greenfield IA) --- */ +.tbDash { + display: flex; + flex-direction: column; + gap: 1.75rem; + padding: 1.25rem 1.5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.tbDashHero { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 1.25rem; + padding: 1.5rem 1.75rem; + border-radius: 12px; + background: linear-gradient(135deg, rgba(74, 144, 217, 0.12) 0%, var(--surface-color, #fff) 48%); + border: 1px solid var(--border-color, #e6e6e6); +} + +.tbDashTitle { + margin: 0 0 0.35rem 0; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #1a1a1a); +} + +.tbDashSubtitle { + margin: 0; + max-width: 520px; + font-size: 0.95rem; + line-height: 1.45; + color: var(--text-secondary, #555); +} + +.tbDashQuickActions { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; +} + +.tbDashBtnPrimary { + padding: 0.65rem 1.35rem; + border-radius: 8px; + border: none; + background: var(--primary-color, #4A90D9); + color: #fff; + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.tbDashBtnPrimary:hover { + background: var(--primary-hover, #3A7BC8); +} + +.tbDashBtnSecondary { + padding: 0.65rem 1.1rem; + border-radius: 8px; + border: 1px solid var(--border-color, #d0d0d0); + background: var(--surface-color, #fff); + color: var(--text-primary, #333); + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; +} + +.tbDashBtnSecondary:hover { + border-color: var(--primary-color, #4A90D9); + color: var(--primary-color, #4A90D9); +} + +.tbDashKpiGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1rem; +} + +.tbDashKpiCard { + padding: 1.1rem 1.25rem; + border-radius: 10px; + border: 1px solid var(--border-color, #e8e8e8); + background: var(--surface-color, #fff); +} + +.tbDashKpiValue { + font-size: 1.75rem; + font-weight: 700; + color: var(--primary-color, #4A90D9); + line-height: 1.1; +} + +.tbDashKpiLabel { + margin-top: 0.35rem; + font-size: 0.82rem; + font-weight: 600; + color: var(--text-secondary, #666); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.tbDashKpiHint { + margin-top: 0.4rem; + font-size: 0.8rem; + color: var(--text-tertiary, #888); +} + +.tbDashSection { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.tbDashSectionHead { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.tbDashSectionTitle { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + color: var(--text-primary, #222); +} + +.tbDashLinkBtn { + padding: 0.35rem 0.75rem; + border: none; + background: transparent; + color: var(--primary-color, #4A90D9); + font-size: 0.88rem; + font-weight: 500; + cursor: pointer; + text-decoration: underline; +} + +.tbDashModuleGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.tbDashModuleCard { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.35rem; + padding: 1rem 1.1rem; + border-radius: 10px; + border: 1px solid var(--border-color, #e6e6e6); + background: var(--surface-color, #fafafa); + cursor: pointer; + text-align: left; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.tbDashModuleCard:hover { + border-color: var(--primary-color, #4A90D9); + box-shadow: 0 2px 8px rgba(74, 144, 217, 0.12); +} + +.tbDashModuleTitle { + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary, #222); +} + +.tbDashModuleCount { + font-size: 0.82rem; + color: var(--text-secondary, #666); +} + +.tbDashSessionList { + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.tbDashSessionRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem 1rem; + padding: 0.85rem 1rem; + border-radius: 10px; + border: 1px solid var(--border-color, #eaeaea); + background: var(--surface-color, #fff); +} + +.tbDashSessionMain { + display: flex; + align-items: center; + gap: 0.6rem; + flex: 1 1 200px; +} + +.tbDashSessionMeta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1rem; + font-size: 0.82rem; + color: var(--text-secondary, #666); + flex: 2 1 220px; +} + +.tbDashSessionActions { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-left: auto; +} diff --git a/src/pages/views/teamsbot/TeamsbotAssistantView.tsx b/src/pages/views/teamsbot/TeamsbotAssistantView.tsx index a6a473b..b80493c 100644 --- a/src/pages/views/teamsbot/TeamsbotAssistantView.tsx +++ b/src/pages/views/teamsbot/TeamsbotAssistantView.tsx @@ -3,10 +3,12 @@ * * Wizard: Select/create module → Meeting link → Bot selection → "Start bot" */ -import React, { useState, useEffect, useCallback } from 'react'; +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'; @@ -18,16 +20,26 @@ export const TeamsbotAssistantView: React.FC = () => { 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(preselectedModuleId ? 'meeting' : 'module'); - const [modules, setModules] = useState([]); + const [modules, setModules] = useState([]); + const [moduleFilter, setModuleFilter] = useState(''); const [selectedModuleId, setSelectedModuleId] = useState(preselectedModuleId); const [newModuleTitle, setNewModuleTitle] = useState(''); const [createNewModule, setCreateNewModule] = useState(false); const [meetingLink, setMeetingLink] = useState(''); const [botName, setBotName] = useState('AI Assistant'); + const [joinMode, setJoinMode] = useState('anonymous'); + const [sessionContext, setSessionContext] = useState(''); + const [userAccount, setUserAccount] = useState(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(null); @@ -45,6 +57,33 @@ export const TeamsbotAssistantView: React.FC = () => { 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(''); + 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]); @@ -60,6 +99,27 @@ export const TeamsbotAssistantView: React.FC = () => { 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 { @@ -73,7 +133,9 @@ export const TeamsbotAssistantView: React.FC = () => { meetingLink: meetingLink.trim(), botName, moduleId: moduleId || undefined, - } as any); + joinMode, + sessionContext: sessionContext.trim() || undefined, + }); navigate(`/mandates/${mandateId}/teamsbot/${instanceId}/sessions?sessionId=${result.session.id}`); } catch (err: any) { @@ -106,16 +168,27 @@ export const TeamsbotAssistantView: React.FC = () => { {t('Bestehendes Modul')} {!createNewModule && ( - + <> + setModuleFilter(e.target.value)} + aria-label={t('Modul suchen')} + /> + + )}