Merge pull request #13 from valueonag/feat/commcoach

fixed build
This commit is contained in:
Patrick Motsch 2026-03-02 21:25:17 +01:00 committed by GitHub
commit 0143358aa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 53 deletions

45
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,45 @@
interface SpeechRecognition extends EventTarget {
continuous: boolean;
interimResults: boolean;
lang: string;
onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
onend: ((this: SpeechRecognition, ev: Event) => void) | null;
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null;
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null;
onspeechstart: ((this: SpeechRecognition, ev: Event) => void) | null;
onspeechend: ((this: SpeechRecognition, ev: Event) => void) | null;
start(): void;
stop(): void;
abort(): void;
}
interface SpeechRecognitionErrorEvent extends Event {
error: string;
message: string;
}
interface SpeechRecognitionEvent extends Event {
resultIndex: number;
results: SpeechRecognitionResultList;
}
interface SpeechRecognitionResultList {
length: number;
[index: number]: SpeechRecognitionResult;
}
interface SpeechRecognitionResult {
isFinal: boolean;
length: number;
[index: number]: SpeechRecognitionAlternative;
}
interface SpeechRecognitionAlternative {
transcript: string;
confidence: number;
}
interface Window {
SpeechRecognition: new () => SpeechRecognition;
webkitSpeechRecognition: new () => SpeechRecognition;
}

View file

@ -9,10 +9,9 @@ import { useApiRequest } from './useApi';
import { useInstanceId } from './useCurrentInstance'; import { useInstanceId } from './useCurrentInstance';
import { import {
getContextsApi, createContextApi, getContextDetailApi, getContextsApi, createContextApi, getContextDetailApi,
startSessionStreamApi, getSessionApi, completeSessionApi, cancelSessionApi, startSessionStreamApi, completeSessionApi, cancelSessionApi,
sendMessageStreamApi, sendAudioStreamApi, sendMessageStreamApi, sendAudioStreamApi,
getTasksApi, createTaskApi, updateTaskStatusApi, deleteTaskApi, createTaskApi, updateTaskStatusApi, deleteTaskApi,
getProfileApi, testVoiceApi,
type CoachingContext, type CoachingSession, type CoachingMessage, type CoachingContext, type CoachingSession, type CoachingMessage,
type CoachingTask, type CoachingScore, type SSEEvent, type CoachingTask, type CoachingScore, type SSEEvent,
} from '../api/commcoachApi'; } from '../api/commcoachApi';
@ -84,45 +83,9 @@ export function useCommcoach(): CommcoachHookReturn {
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
const currentAudioRef = useRef<HTMLAudioElement | null>(null); const currentAudioRef = useRef<HTMLAudioElement | null>(null);
const isTtsPlayingRef = useRef(false); const isTtsPlayingRef = useRef(false);
const profileRef = useRef<{ preferredLanguage?: string; preferredVoice?: string } | null>(null);
useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []);
const _speakText = useCallback(async (text: string) => {
if (!instanceId) return;
const plain = _stripMarkdownForTts(text);
if (!plain.trim()) return;
if (currentAudioRef.current) {
currentAudioRef.current.pause();
currentAudioRef.current = null;
}
try {
let profile = profileRef.current;
if (!profile) {
const p = await getProfileApi(request, instanceId);
profile = { preferredLanguage: p?.preferredLanguage || 'de-DE', preferredVoice: p?.preferredVoice };
profileRef.current = profile;
}
const lang = profile?.preferredLanguage || 'de-DE';
const voiceId = profile?.preferredVoice || undefined;
const result = await testVoiceApi(request, instanceId, { text: plain, language: lang, voiceId });
if (result?.success && result?.audio && isMountedRef.current) {
const audio = new Audio(`data:audio/mp3;base64,${result.audio}`);
currentAudioRef.current = audio;
audio.onended = () => { currentAudioRef.current = null; };
try {
await audio.play();
} catch (playErr: any) {
if (playErr?.name === 'NotAllowedError') {
console.warn('CommCoach TTS: Browser blocked audio. Click "Session starten" or "Senden" first.');
}
}
}
} catch {
// TTS failed silently, text is still visible
}
}, [request, instanceId]);
const refreshContexts = useCallback(async () => { const refreshContexts = useCallback(async () => {
if (!instanceId) return; if (!instanceId) return;
setLoadingContexts(true); setLoadingContexts(true);
@ -520,8 +483,6 @@ export function useCommcoach(): CommcoachHookReturn {
useEffect(() => { if (instanceId) refreshContexts(); }, [instanceId, refreshContexts]); useEffect(() => { if (instanceId) refreshContexts(); }, [instanceId, refreshContexts]);
useEffect(() => { profileRef.current = null; }, [instanceId]);
return { return {
contexts, selectedContextId, selectedContext, loadingContexts, contexts, selectedContextId, selectedContext, loadingContexts,
session, messages, isStreaming, streamingStatus, session, messages, isStreaming, streamingStatus,
@ -536,16 +497,6 @@ export function useCommcoach(): CommcoachHookReturn {
}; };
} }
function _stripMarkdownForTts(text: string): string {
return text
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/\[(.+?)\]\(.+?\)/g, '$1')
.replace(/^#+\s*/gm, '')
.replace(/`[^`]+`/g, (m) => m.slice(1, -1))
.trim();
}
async function _unlockAudioForTts(): Promise<void> { async function _unlockAudioForTts(): Promise<void> {
try { try {
const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();

View file

@ -13,7 +13,7 @@ import styles from './CommcoachDashboardView.module.css';
export const CommcoachDashboardView: React.FC = () => { export const CommcoachDashboardView: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { mandateId, instanceId } = useCurrentInstance(); const { mandateId, instanceId } = useCurrentInstance();
const { dashboard, profile, loading, error, refresh } = useCommcoachDashboard(); const { dashboard, loading, error } = useCommcoachDashboard();
const handleContextClick = (contextId: string) => { const handleContextClick = (contextId: string) => {
if (mandateId && instanceId) { if (mandateId && instanceId) {

View file

@ -209,7 +209,7 @@ export const CommcoachDossierView: React.FC = () => {
interface ScoreGroup { interface ScoreGroup {
dimension: string; dimension: string;
latest: { score: number; trend: string; evidence?: string }; latest: { score: number; trend: string; evidence?: string; createdAt?: string };
history: Array<{ score: number; createdAt?: string }>; history: Array<{ score: number; createdAt?: string }>;
} }