606 lines
22 KiB
TypeScript
606 lines
22 KiB
TypeScript
/**
|
|
* useCommcoach Hook
|
|
*
|
|
* State management for CommCoach coaching sessions, contexts, and chat.
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback, useRef, type MutableRefObject } from 'react';
|
|
import { useApiRequest } from './useApi';
|
|
import { useInstanceId } from './useCurrentInstance';
|
|
import {
|
|
getContextsApi, createContextApi, getContextDetailApi,
|
|
startSessionStreamApi, completeSessionApi, cancelSessionApi,
|
|
sendMessageStreamApi, sendAudioStreamApi,
|
|
createTaskApi, updateTaskStatusApi, deleteTaskApi,
|
|
type CoachingContext, type CoachingSession, type CoachingMessage,
|
|
type CoachingTask, type CoachingScore, type SSEEvent,
|
|
type SendMessageOptions,
|
|
} from '../api/commcoachApi';
|
|
import { useTtsPlayback, type TtsEvent } from './useTtsPlayback';
|
|
|
|
export interface CommcoachHookReturn {
|
|
contexts: CoachingContext[];
|
|
selectedContextId: string | null;
|
|
selectedContext: CoachingContext | null;
|
|
loadingContexts: boolean;
|
|
|
|
session: CoachingSession | null;
|
|
messages: CoachingMessage[];
|
|
isStreaming: boolean;
|
|
streamingStatus: string | null;
|
|
streamingMessage: string | null;
|
|
|
|
tasks: CoachingTask[];
|
|
scores: CoachingScore[];
|
|
sessions: CoachingSession[];
|
|
|
|
error: string | null;
|
|
inputValue: string;
|
|
setInputValue: (v: string) => void;
|
|
|
|
agentToolCalls: Array<{ toolName: string; args?: Record<string, unknown>; result?: string; success?: boolean }>;
|
|
|
|
selectContext: (contextId: string, options?: { skipSessionResume?: boolean }) => Promise<void>;
|
|
createContext: (title: string, description?: string, category?: string, goals?: string[]) => Promise<void>;
|
|
archiveContext: (contextId: string) => Promise<void>;
|
|
|
|
startSession: (personaId?: string) => Promise<void>;
|
|
sendMessage: (content: string, options?: SendMessageOptions) => Promise<void>;
|
|
sendAudio: (audioBlob: Blob) => Promise<void>;
|
|
completeSession: () => Promise<void>;
|
|
cancelSession: () => Promise<void>;
|
|
|
|
stopTts: () => void;
|
|
pauseTts: () => void;
|
|
resumeTts: () => void;
|
|
hasAudioToResume: () => boolean;
|
|
ttsIsPlaying: boolean;
|
|
ttsIsPaused: boolean;
|
|
|
|
onTtsEventRef: MutableRefObject<((event: TtsEvent) => void) | null>;
|
|
|
|
actionLoading: string | null;
|
|
|
|
toggleTaskStatus: (taskId: string, currentStatus: string) => Promise<void>;
|
|
addTask: (title: string, description?: string) => Promise<void>;
|
|
removeTask: (taskId: string) => Promise<void>;
|
|
|
|
onDocumentCreatedRef: MutableRefObject<((doc: any) => void) | null>;
|
|
|
|
refreshContexts: () => Promise<void>;
|
|
}
|
|
|
|
export function useCommcoach(instanceIdOverride?: string): CommcoachHookReturn {
|
|
const { request } = useApiRequest();
|
|
const routeInstanceId = useInstanceId();
|
|
const instanceId = instanceIdOverride || routeInstanceId;
|
|
|
|
const [contexts, setContexts] = useState<CoachingContext[]>([]);
|
|
const [selectedContextId, setSelectedContextId] = useState<string | null>(null);
|
|
const [selectedContext, setSelectedContext] = useState<CoachingContext | null>(null);
|
|
const [loadingContexts, setLoadingContexts] = useState(false);
|
|
|
|
const [session, setSession] = useState<CoachingSession | null>(null);
|
|
const [messages, setMessages] = useState<CoachingMessage[]>([]);
|
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
const [streamingStatus, setStreamingStatus] = useState<string | null>(null);
|
|
const [streamingMessage, setStreamingMessage] = useState<string | null>(null);
|
|
|
|
const [tasks, setTasks] = useState<CoachingTask[]>([]);
|
|
const [scores, setScores] = useState<CoachingScore[]>([]);
|
|
const [sessions, setSessions] = useState<CoachingSession[]>([]);
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [inputValue, setInputValue] = useState('');
|
|
const [agentToolCalls, setAgentToolCalls] = useState<Array<{ toolName: string; args?: Record<string, unknown>; result?: string; success?: boolean }>>([]);
|
|
|
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
|
|
const isMountedRef = useRef(true);
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
const onTtsEventRef = useRef<((event: TtsEvent) => void) | null>(null);
|
|
const onDocumentCreatedRef = useRef<((doc: any) => void) | null>(null);
|
|
|
|
const ttsPlayback = useTtsPlayback({
|
|
onPlaying: () => { (window as any).__dlog?.('TTS-PLAYING'); onTtsEventRef.current?.('playing'); },
|
|
onEnded: () => { (window as any).__dlog?.('TTS-ENDED'); onTtsEventRef.current?.('ended'); },
|
|
onPaused: () => { (window as any).__dlog?.('TTS-PAUSED'); onTtsEventRef.current?.('paused'); },
|
|
onError: () => { (window as any).__dlog?.('TTS-ERROR'); onTtsEventRef.current?.('error'); },
|
|
});
|
|
|
|
useEffect(() => {
|
|
isMountedRef.current = true;
|
|
return () => { isMountedRef.current = false; };
|
|
}, []);
|
|
|
|
const refreshContexts = useCallback(async () => {
|
|
if (!instanceId) return;
|
|
setLoadingContexts(true);
|
|
setError(null);
|
|
try {
|
|
const data = await getContextsApi(request, instanceId);
|
|
if (isMountedRef.current) setContexts(data);
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Laden der Kontexte');
|
|
} finally {
|
|
if (isMountedRef.current) setLoadingContexts(false);
|
|
}
|
|
}, [request, instanceId]);
|
|
|
|
const stopTts = useCallback(() => {
|
|
ttsPlayback.stop();
|
|
}, [ttsPlayback]);
|
|
|
|
const pauseTts = useCallback(() => {
|
|
ttsPlayback.pause();
|
|
}, [ttsPlayback]);
|
|
|
|
const resumeTts = useCallback(() => {
|
|
ttsPlayback.resume();
|
|
}, [ttsPlayback]);
|
|
|
|
const hasAudioToResume = useCallback(() => {
|
|
return ttsPlayback.isPaused;
|
|
}, [ttsPlayback]);
|
|
|
|
const selectContext = useCallback(async (contextId: string, options?: { skipSessionResume?: boolean }) => {
|
|
if (!instanceId) return;
|
|
setSelectedContextId(contextId);
|
|
setError(null);
|
|
try {
|
|
const detail = await getContextDetailApi(request, instanceId, contextId);
|
|
if (!isMountedRef.current) return;
|
|
setSelectedContext(detail.context);
|
|
setTasks(detail.tasks || []);
|
|
setScores(detail.scores || []);
|
|
setSessions(detail.sessions || []);
|
|
|
|
if (options?.skipSessionResume) {
|
|
setSession(null);
|
|
setMessages([]);
|
|
return;
|
|
}
|
|
|
|
const activeSession = detail.sessions?.find((s: CoachingSession) => s.status === 'active');
|
|
if (activeSession) {
|
|
await _unlockAudioForTts();
|
|
setIsStreaming(true);
|
|
await startSessionStreamApi(
|
|
instanceId,
|
|
contextId,
|
|
(event: SSEEvent) => {
|
|
if (!isMountedRef.current) return;
|
|
const eventType = event.type;
|
|
const eventData = event.data;
|
|
if (eventType === 'sessionState' && eventData) {
|
|
const sess = eventData.session;
|
|
if (sess) setSession(sess);
|
|
if (eventData.resumed && Array.isArray(eventData.messages)) {
|
|
setMessages(eventData.messages);
|
|
}
|
|
} else if (eventType === 'ttsAudio' && eventData?.audio) {
|
|
ttsPlayback.play(eventData.audio);
|
|
}
|
|
if (eventType === 'complete') setIsStreaming(false);
|
|
},
|
|
(err) => { if (isMountedRef.current) { setError(err.message); setIsStreaming(false); } },
|
|
() => { if (isMountedRef.current) setIsStreaming(false); },
|
|
);
|
|
} else {
|
|
setSession(null);
|
|
setMessages([]);
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Laden des Kontexts');
|
|
}
|
|
}, [request, instanceId, ttsPlayback.play]);
|
|
|
|
const createContext = useCallback(async (title: string, description?: string, category?: string, goals?: string[]) => {
|
|
if (!instanceId) return;
|
|
setActionLoading('creating');
|
|
try {
|
|
const created = await createContextApi(request, instanceId, { title, description, category, goals });
|
|
if (isMountedRef.current) {
|
|
await refreshContexts();
|
|
setSelectedContextId(created.id);
|
|
setSelectedContext(created);
|
|
setTasks([]);
|
|
setScores([]);
|
|
setSessions([]);
|
|
setSession(null);
|
|
setMessages([]);
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Erstellen des Kontexts');
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId, refreshContexts]);
|
|
|
|
const archiveContext = useCallback(async (contextId: string) => {
|
|
if (!instanceId) return;
|
|
setActionLoading('archiving');
|
|
try {
|
|
const { archiveContextApi } = await import('../api/commcoachApi');
|
|
await archiveContextApi(request, instanceId, contextId);
|
|
if (isMountedRef.current) {
|
|
await refreshContexts();
|
|
if (selectedContextId === contextId) {
|
|
setSelectedContextId(null);
|
|
setSelectedContext(null);
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Archivieren');
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId, selectedContextId, refreshContexts]);
|
|
|
|
const startSessionCb = useCallback(async (personaId?: string) => {
|
|
if (!instanceId || !selectedContextId) return;
|
|
setActionLoading('starting');
|
|
await _unlockAudioForTts();
|
|
setError(null);
|
|
setIsStreaming(true);
|
|
setStreamingStatus(null);
|
|
setStreamingMessage(null);
|
|
setMessages([]);
|
|
setSession(null);
|
|
try {
|
|
await startSessionStreamApi(
|
|
instanceId,
|
|
selectedContextId,
|
|
(event: SSEEvent) => {
|
|
if (!isMountedRef.current) return;
|
|
const eventType = event.type;
|
|
const eventData = event.data;
|
|
|
|
if (eventType === 'sessionState' && eventData) {
|
|
const sess = eventData.session;
|
|
if (sess) {
|
|
setSession(sess);
|
|
}
|
|
if (eventData.resumed && Array.isArray(eventData.messages)) {
|
|
setMessages(eventData.messages);
|
|
}
|
|
} else if (eventType === 'messageChunk' && eventData) {
|
|
setStreamingStatus(prev => prev || 'Coach formuliert Antwort...');
|
|
} else if (eventType === 'message' && eventData) {
|
|
setStreamingMessage(null);
|
|
const msg: CoachingMessage = {
|
|
id: eventData.id || `msg-${Date.now()}`,
|
|
sessionId: eventData.sessionId || '',
|
|
contextId: eventData.contextId || '',
|
|
role: eventData.role,
|
|
content: eventData.content,
|
|
contentType: eventData.contentType || 'text',
|
|
createdAt: eventData.createdAt,
|
|
};
|
|
setMessages(prev => {
|
|
if (prev.some(m => m.id === msg.id)) return prev;
|
|
return [...prev, msg];
|
|
});
|
|
} else if (eventType === 'ttsAudio' && eventData?.audio) {
|
|
ttsPlayback.play(eventData.audio);
|
|
} else if (eventType === 'status' && eventData) {
|
|
setStreamingStatus(eventData.label || null);
|
|
} else if (eventType === 'taskCreated' && eventData) {
|
|
setTasks(prev => [eventData, ...prev]);
|
|
} else if (eventType === 'documentCreated' && eventData) {
|
|
onDocumentCreatedRef.current?.(eventData);
|
|
} else if (eventType === 'error' && eventData) {
|
|
setError(eventData.message || 'Stream-Fehler');
|
|
}
|
|
},
|
|
(err) => {
|
|
if (isMountedRef.current) {
|
|
setError(err.message);
|
|
setIsStreaming(false);
|
|
setStreamingMessage(null);
|
|
}
|
|
},
|
|
() => {
|
|
if (isMountedRef.current) {
|
|
setIsStreaming(false);
|
|
setStreamingStatus(null);
|
|
setStreamingMessage(null);
|
|
}
|
|
},
|
|
personaId,
|
|
);
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) {
|
|
setError(err.message || 'Fehler beim Starten der Session');
|
|
setIsStreaming(false);
|
|
}
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [instanceId, selectedContextId, ttsPlayback.play]);
|
|
|
|
const sendMessage = useCallback(async (content: string, options?: SendMessageOptions) => {
|
|
const normalizedContent = content.trim();
|
|
if (!normalizedContent || !instanceId || !session) return;
|
|
|
|
abortControllerRef.current?.abort();
|
|
const ac = new AbortController();
|
|
abortControllerRef.current = ac;
|
|
|
|
ttsPlayback.stop();
|
|
await _unlockAudioForTts();
|
|
setError(null);
|
|
setIsStreaming(true);
|
|
setStreamingStatus(null);
|
|
setStreamingMessage(null);
|
|
setAgentToolCalls([]);
|
|
|
|
const tempMsg: CoachingMessage = {
|
|
id: `temp-${Date.now()}`,
|
|
sessionId: session.id,
|
|
contextId: session.contextId,
|
|
role: 'user',
|
|
content: normalizedContent,
|
|
contentType: 'text',
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
setMessages(prev => [...prev, tempMsg]);
|
|
setInputValue('');
|
|
|
|
try {
|
|
await sendMessageStreamApi(
|
|
instanceId,
|
|
session.id,
|
|
normalizedContent,
|
|
(event: SSEEvent) => {
|
|
if (!isMountedRef.current || ac.signal.aborted) return;
|
|
const eventType = event.type;
|
|
const eventData = event.data;
|
|
|
|
if (eventType === 'messageChunk' && eventData) {
|
|
setStreamingStatus(prev => prev || 'Coach formuliert Antwort...');
|
|
} else if (eventType === 'message' && eventData) {
|
|
setStreamingMessage(null);
|
|
const msg: CoachingMessage = {
|
|
id: eventData.id || `msg-${Date.now()}`,
|
|
sessionId: session.id,
|
|
contextId: session.contextId,
|
|
role: eventData.role,
|
|
content: eventData.content,
|
|
contentType: 'text',
|
|
createdAt: eventData.createdAt,
|
|
};
|
|
setMessages(prev => {
|
|
if (msg.role === 'user') {
|
|
return prev.map(m => m.id === tempMsg.id ? msg : m);
|
|
}
|
|
if (prev.some(m => m.id === msg.id)) return prev;
|
|
return [...prev, msg];
|
|
});
|
|
} else if (eventType === 'ttsAudio' && eventData?.audio) {
|
|
setError(null);
|
|
ttsPlayback.play(eventData.audio);
|
|
} else if (eventType === 'status' && eventData) {
|
|
setStreamingStatus(eventData.label || null);
|
|
} else if (eventType === 'toolCall' && eventData) {
|
|
setAgentToolCalls(prev => [...prev, { toolName: eventData.toolName, args: eventData.args }]);
|
|
setStreamingStatus(`Tool: ${eventData.toolName}...`);
|
|
} else if (eventType === 'toolResult' && eventData) {
|
|
setAgentToolCalls(prev => prev.map((tc, idx) =>
|
|
idx === prev.length - 1
|
|
? { ...tc, result: eventData.data?.slice(0, 200), success: eventData.success }
|
|
: tc
|
|
));
|
|
} else if (eventType === 'agentProgress' && eventData) {
|
|
setStreamingStatus(`Runde ${eventData.round}/${eventData.maxRounds}...`);
|
|
} else if (eventType === 'taskCreated' && eventData) {
|
|
setTasks(prev => [eventData, ...prev]);
|
|
} else if (eventType === 'documentCreated' && eventData) {
|
|
onDocumentCreatedRef.current?.(eventData);
|
|
} else if (eventType === 'scoreUpdate') {
|
|
// Will refresh on complete
|
|
} else if (eventType === 'error' && eventData) {
|
|
setError(eventData.message || 'Stream-Fehler');
|
|
}
|
|
},
|
|
(err) => {
|
|
if (err.name === 'AbortError') return;
|
|
if (isMountedRef.current) {
|
|
setError(err.message);
|
|
setIsStreaming(false);
|
|
setStreamingMessage(null);
|
|
}
|
|
},
|
|
() => {
|
|
if (isMountedRef.current) {
|
|
setIsStreaming(false);
|
|
setStreamingStatus(null);
|
|
setStreamingMessage(null);
|
|
}
|
|
},
|
|
ac.signal,
|
|
options,
|
|
);
|
|
} catch (err: any) {
|
|
if (err.name === 'AbortError') return;
|
|
if (isMountedRef.current) {
|
|
setError(err.message);
|
|
setIsStreaming(false);
|
|
}
|
|
}
|
|
}, [instanceId, session, ttsPlayback.play]);
|
|
|
|
const sendAudio = useCallback(async (audioBlob: Blob) => {
|
|
if (!instanceId || !session) return;
|
|
ttsPlayback.stop();
|
|
await _unlockAudioForTts();
|
|
setError(null);
|
|
setIsStreaming(true);
|
|
setStreamingStatus(null);
|
|
setStreamingMessage(null);
|
|
try {
|
|
await sendAudioStreamApi(
|
|
instanceId,
|
|
session.id,
|
|
audioBlob,
|
|
(event: SSEEvent) => {
|
|
if (!isMountedRef.current) return;
|
|
const eventType = event.type;
|
|
const eventData = event.data;
|
|
|
|
if (eventType === 'messageChunk' && eventData) {
|
|
setStreamingStatus(prev => prev || 'Coach formuliert Antwort...');
|
|
} else if (eventType === 'status' && eventData) {
|
|
setStreamingStatus(eventData.label || null);
|
|
} else if (eventType === 'message' && eventData) {
|
|
if (eventData.role === 'assistant') setError(null);
|
|
const msg: CoachingMessage = {
|
|
id: eventData.id || `msg-${Date.now()}`,
|
|
sessionId: session.id,
|
|
contextId: session.contextId,
|
|
role: eventData.role,
|
|
content: eventData.content,
|
|
contentType: eventData.contentType || 'text',
|
|
createdAt: eventData.createdAt,
|
|
};
|
|
setMessages(prev => {
|
|
if (prev.some(m => m.id === msg.id)) return prev;
|
|
return [...prev, msg];
|
|
});
|
|
} else if (eventType === 'ttsAudio' && eventData?.audio) {
|
|
setError(null);
|
|
ttsPlayback.play(eventData.audio);
|
|
} else if (eventType === 'taskCreated' && eventData) {
|
|
setTasks(prev => [eventData, ...prev]);
|
|
} else if (eventType === 'documentCreated' && eventData) {
|
|
onDocumentCreatedRef.current?.(eventData);
|
|
} else if (eventType === 'error' && eventData) {
|
|
setError(eventData.message || 'Audio-Fehler');
|
|
}
|
|
},
|
|
(err) => {
|
|
if (isMountedRef.current) {
|
|
setError(err.message);
|
|
setIsStreaming(false);
|
|
}
|
|
},
|
|
() => {
|
|
if (isMountedRef.current) {
|
|
setIsStreaming(false);
|
|
setStreamingStatus(null);
|
|
}
|
|
},
|
|
);
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) {
|
|
setError(err.message);
|
|
setIsStreaming(false);
|
|
}
|
|
}
|
|
}, [instanceId, session]);
|
|
|
|
const completeSessionCb = useCallback(async () => {
|
|
if (!instanceId || !session) return;
|
|
setActionLoading('completing');
|
|
try {
|
|
const completed = await completeSessionApi(request, instanceId, session.id);
|
|
if (isMountedRef.current) {
|
|
setMessages([]);
|
|
setSession(completed);
|
|
if (selectedContextId) await selectContext(selectedContextId, { skipSessionResume: true });
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Abschliessen');
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId, session, selectedContextId, selectContext]);
|
|
|
|
const cancelSessionCb = useCallback(async () => {
|
|
if (!instanceId || !session) return;
|
|
setActionLoading('cancelling');
|
|
try {
|
|
await cancelSessionApi(request, instanceId, session.id);
|
|
if (isMountedRef.current) {
|
|
setSession(null);
|
|
setMessages([]);
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message || 'Fehler beim Abbrechen');
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId, session]);
|
|
|
|
const toggleTaskStatus = useCallback(async (taskId: string, currentStatus: string) => {
|
|
if (!instanceId) return;
|
|
setActionLoading('togglingTask');
|
|
const newStatus = currentStatus === 'done' ? 'open' : 'done';
|
|
try {
|
|
const updated = await updateTaskStatusApi(request, instanceId, taskId, newStatus);
|
|
if (isMountedRef.current) {
|
|
setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
|
|
}
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message);
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId]);
|
|
|
|
const addTask = useCallback(async (title: string, description?: string) => {
|
|
if (!instanceId || !selectedContextId) return;
|
|
setActionLoading('addingTask');
|
|
try {
|
|
const created = await createTaskApi(request, instanceId, selectedContextId, { title, description });
|
|
if (isMountedRef.current) setTasks(prev => [created, ...prev]);
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message);
|
|
} finally {
|
|
if (isMountedRef.current) setActionLoading(null);
|
|
}
|
|
}, [request, instanceId, selectedContextId]);
|
|
|
|
const removeTask = useCallback(async (taskId: string) => {
|
|
if (!instanceId) return;
|
|
try {
|
|
await deleteTaskApi(request, instanceId, taskId);
|
|
if (isMountedRef.current) setTasks(prev => prev.filter(t => t.id !== taskId));
|
|
} catch (err: any) {
|
|
if (isMountedRef.current) setError(err.message);
|
|
}
|
|
}, [request, instanceId]);
|
|
|
|
useEffect(() => { if (instanceId) refreshContexts(); }, [instanceId, refreshContexts]);
|
|
|
|
return {
|
|
contexts, selectedContextId, selectedContext, loadingContexts,
|
|
session, messages, isStreaming, streamingStatus, streamingMessage,
|
|
tasks, scores, sessions,
|
|
error, inputValue, setInputValue,
|
|
agentToolCalls,
|
|
selectContext, createContext, archiveContext,
|
|
startSession: startSessionCb,
|
|
sendMessage, sendAudio,
|
|
completeSession: completeSessionCb, cancelSession: cancelSessionCb,
|
|
stopTts, pauseTts, resumeTts, hasAudioToResume,
|
|
ttsIsPlaying: ttsPlayback.isPlaying, ttsIsPaused: ttsPlayback.isPaused,
|
|
onTtsEventRef,
|
|
actionLoading,
|
|
toggleTaskStatus, addTask, removeTask,
|
|
onDocumentCreatedRef,
|
|
refreshContexts,
|
|
};
|
|
}
|
|
|
|
async function _unlockAudioForTts(): Promise<void> {
|
|
try {
|
|
const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
if (ctx.state === 'suspended') await ctx.resume();
|
|
const silent = new Audio('data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=');
|
|
silent.volume = 0;
|
|
await silent.play();
|
|
} catch {
|
|
// Ignore if audio unlock fails
|
|
}
|
|
}
|