Skip to content

Commit 1a864f5

Browse files
committed
fix: risolti problemi con la chat vocale e messo il tutorial dopo il login
1 parent 5d6d433 commit 1a864f5

5 files changed

Lines changed: 208 additions & 51 deletions

File tree

src/components/BotChat/VoiceChatModal.tsx

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,25 @@ const VoiceChatModal: React.FC<VoiceChatModalProps> = ({
249249
const stateTextOpacity = useRef(new Animated.Value(1)).current;
250250
const prevStateRef = useRef(state);
251251

252+
// Storico errori — persiste anche quando state cambia
253+
const [errorLog, setErrorLog] = useState<string[]>([]);
254+
const prevErrorRef = useRef<string | null>(null);
255+
useEffect(() => {
256+
if (error && error !== prevErrorRef.current) {
257+
prevErrorRef.current = error;
258+
setErrorLog(prev => [...prev, error]);
259+
console.log('[VoiceChatModal] Errore ricevuto:', error);
260+
}
261+
}, [error]);
262+
263+
// Reset log quando il modal si chiude
264+
useEffect(() => {
265+
if (!visible) {
266+
setErrorLog([]);
267+
prevErrorRef.current = null;
268+
}
269+
}, [visible]);
270+
252271
// Gestione connessione
253272
const handleConnect = useCallback(async () => {
254273
if (!hasPermissions) {
@@ -287,9 +306,12 @@ const VoiceChatModal: React.FC<VoiceChatModalProps> = ({
287306
}
288307
}, [visible, state, handleConnect]);
289308

290-
// Cleanup quando il modal si chiude
309+
// Cleanup quando il modal si chiude (solo dopo la prima apertura, non al mount iniziale)
310+
const hasEverOpenedRef = useRef(false);
291311
useEffect(() => {
292-
if (!visible) {
312+
if (visible) {
313+
hasEverOpenedRef.current = true;
314+
} else if (hasEverOpenedRef.current) {
293315
disconnect();
294316
}
295317
}, [visible, disconnect]);
@@ -458,9 +480,15 @@ const VoiceChatModal: React.FC<VoiceChatModalProps> = ({
458480
</Animated.View>
459481
)}
460482

461-
{/* Errore */}
462-
{state === 'error' && error && (
463-
<Text style={styles.errorText}>{error}</Text>
483+
{/* Log errori — sempre visibile quando presenti */}
484+
{errorLog.length > 0 && (
485+
<View style={styles.errorLog}>
486+
{errorLog.map((msg, i) => (
487+
<Text key={i} style={styles.errorLogText}>
488+
{'\u25CF'} {msg}
489+
</Text>
490+
))}
491+
</View>
464492
)}
465493
</View>
466494

@@ -696,6 +724,21 @@ const styles = StyleSheet.create({
696724
textAlign: "center",
697725
paddingHorizontal: 24,
698726
},
727+
errorLog: {
728+
marginTop: 12,
729+
width: "100%",
730+
backgroundColor: "rgba(255, 69, 58, 0.07)",
731+
borderRadius: 12,
732+
paddingHorizontal: 14,
733+
paddingVertical: 10,
734+
gap: 4,
735+
},
736+
errorLogText: {
737+
fontSize: 12,
738+
color: "#FF453A",
739+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
740+
lineHeight: 18,
741+
},
699742

700743
// Control bar
701744
controlBar: {

src/contexts/TutorialContext.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface TutorialContextType {
1616
startTutorial: () => void;
1717
closeTutorial: () => void;
1818
skipTutorial: () => void;
19+
triggerPostLoginTutorial: () => Promise<void>;
1920
}
2021

2122
const TutorialContext = createContext<TutorialContextType | undefined>(
@@ -27,22 +28,18 @@ export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({
2728
}) => {
2829
const [isTutorialVisible, setIsTutorialVisible] = useState(false);
2930

30-
// Check if tutorial should auto-start on first launch
31-
React.useEffect(() => {
32-
const checkTutorialStatus = async () => {
33-
try {
34-
const status = await AsyncStorage.getItem(TUTORIAL_STORAGE_KEY);
35-
const hasCompleted = status === "true" || status === "skipped";
31+
// Mostra il tutorial solo dopo il login, se non è già stato completato
32+
const triggerPostLoginTutorial = useCallback(async () => {
33+
try {
34+
const status = await AsyncStorage.getItem(TUTORIAL_STORAGE_KEY);
35+
const hasCompleted = status === "true" || status === "skipped";
3636

37-
if (!hasCompleted) {
38-
setIsTutorialVisible(true);
39-
}
40-
} catch (error) {
41-
console.error("[TUTORIAL] Error checking tutorial status:", error);
37+
if (!hasCompleted) {
38+
setIsTutorialVisible(true);
4239
}
43-
};
44-
45-
checkTutorialStatus();
40+
} catch (error) {
41+
console.error("[TUTORIAL] Error checking tutorial status:", error);
42+
}
4643
}, []);
4744

4845
const startTutorial = useCallback(() => {
@@ -77,6 +74,7 @@ export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({
7774
startTutorial,
7875
closeTutorial,
7976
skipTutorial,
77+
triggerPostLoginTutorial,
8078
}}
8179
>
8280
{children}
@@ -98,6 +96,8 @@ export const useTutorialContext = () => {
9896
console.warn("[TUTORIAL] closeTutorial called but context not available"),
9997
skipTutorial: () =>
10098
console.warn("[TUTORIAL] skipTutorial called but context not available"),
99+
triggerPostLoginTutorial: async () =>
100+
console.warn("[TUTORIAL] triggerPostLoginTutorial called but context not available"),
101101
};
102102
}
103103
return context;

src/hooks/useVoiceChat.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,32 @@ export function useVoiceChat() {
106106
}, []);
107107

108108
/**
109-
* Callback per gestire i messaggi WebSocket
109+
* Ref stabile per le callback WebSocket.
110+
* Viene aggiornato ad ogni render in modo che VoiceBotWebSocket usi sempre
111+
* le callback correnti, anche dopo chiusura e riapertura del modal.
112+
* Questo risolve il problema "schermata statica ai numeri pari" causato da
113+
* closure stale catturate in initialize() al primo render.
110114
*/
111-
const websocketCallbacks: VoiceChatCallbacks = {
115+
const websocketCallbacksRef = useRef<VoiceChatCallbacks>({});
116+
117+
// Proxy stabile passato al costruttore VoiceBotWebSocket: delega sempre al ref corrente
118+
const stableCallbacks = useRef<VoiceChatCallbacks>({
119+
onConnectionOpen: (...args) => websocketCallbacksRef.current.onConnectionOpen?.(...args),
120+
onAuthenticationSuccess:(...args) => websocketCallbacksRef.current.onAuthenticationSuccess?.(...args),
121+
onReady: (...args) => websocketCallbacksRef.current.onReady?.(...args),
122+
onAuthenticationFailed: (...args) => websocketCallbacksRef.current.onAuthenticationFailed?.(...args),
123+
onConnectionClose: (...args) => websocketCallbacksRef.current.onConnectionClose?.(...args),
124+
onStatus: (...args) => websocketCallbacksRef.current.onStatus?.(...args),
125+
onAudioChunk: (...args) => websocketCallbacksRef.current.onAudioChunk?.(...args),
126+
onTranscript: (...args) => websocketCallbacksRef.current.onTranscript?.(...args),
127+
onToolCall: (...args) => websocketCallbacksRef.current.onToolCall?.(...args),
128+
onToolOutput: (...args) => websocketCallbacksRef.current.onToolOutput?.(...args),
129+
onDone: (...args) => websocketCallbacksRef.current.onDone?.(...args),
130+
onError: (...args) => websocketCallbacksRef.current.onError?.(...args),
131+
}).current;
132+
133+
// Aggiorna il ref ad ogni render con le callback che chiudono su stato/ref correnti
134+
websocketCallbacksRef.current = {
112135
onConnectionOpen: () => {
113136
if (!isMountedRef.current) return;
114137
setState('authenticating');
@@ -253,19 +276,26 @@ export function useVoiceChat() {
253276
isMutedRef.current = false;
254277

255278
// Controlla se l'agente ha prodotto audio (risposta legittima) o era vuoto (ciclo spurio).
256-
// I cicli spurii si verificano quando il server elabora audio residuo/rumore di avvio
257-
// del microfono invece di vero parlato dell'utente (es. dopo chiamate MCP).
279+
// Un ciclo agente senza audio indica una fase di pianificazione tool call:
280+
// il modello ha deciso cosa fare ma non ha ancora parlato. Riavviare il mic
281+
// in questo momento crea una finestra (~374ms) in cui il mic invia audio
282+
// mentre il tool è in esecuzione → VAD trigge → risposta del secondo
283+
// ciclo interrotta prima ancora di partire.
284+
// Fix: al primo ciclo senza audio non riavviare il mic; il secondo ciclo
285+
// (con audio) gestirà correttamente il riavvio.
258286
if (!agentHadAudioRef.current) {
259287
consecutiveEmptyAgentsRef.current++;
260288
} else {
261289
consecutiveEmptyAgentsRef.current = 0;
262290
}
263291

264-
// Se troppi cicli agente-vuoti consecutivi, non riavviare automaticamente.
265-
// L'utente dovrà parlare manualmente (mic è già in stato 'ready').
266-
if (consecutiveEmptyAgentsRef.current >= 3) {
292+
// Non riavviare il mic se questo ciclo agente non ha prodotto audio.
293+
// Copre sia i cicli di pianificazione tool (1 ciclo vuoto) sia eventuali
294+
// cicli spurii multipli consecutivi.
295+
if (consecutiveEmptyAgentsRef.current >= 1) {
267296
consecutiveEmptyAgentsRef.current = 0;
268-
// Non riavviare il mic: l'utente deve parlare esplicitamente
297+
// Non riavviare il mic: l'agente sta ancora elaborando (tool call in corso).
298+
// Il ciclo successivo con audio gestirà il riavvio.
269299
break;
270300
}
271301

@@ -456,7 +486,7 @@ export function useVoiceChat() {
456486

457487
audioRecorderRef.current = new AudioRecorder();
458488
audioPlayerRef.current = new AudioPlayer();
459-
websocketRef.current = new VoiceBotWebSocket(websocketCallbacks);
489+
websocketRef.current = new VoiceBotWebSocket(stableCallbacks);
460490

461491
// Pre-inizializza TrackPlayer per evitare ritardi al primo chunk audio.
462492
// Questo elimina la race condition dove audio_end/agent_end arrivano

src/navigation/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { useTranslation } from 'react-i18next';
5555

5656
// Definizione del tipo per le route dello Stack principale
5757
export type RootStackParamList = {
58+
WelcomeCarousel: undefined;
5859
Login: undefined;
5960
Register: undefined;
6061
EmailVerification: { email: string; username: string; password: string };
@@ -238,6 +239,7 @@ function AppStack() {
238239
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
239240
const [hasSeenWelcome, setHasSeenWelcome] = useState<boolean | null>(null);
240241
const [isLoading, setIsLoading] = useState(true); // Controlla lo stato di autenticazione all'avvio
242+
const { triggerPostLoginTutorial } = useTutorialContext();
241243

242244
// 🔔 Inizializza il sistema di notifiche quando l'utente è autenticato
243245
const { notification } = useNotifications();
@@ -336,6 +338,7 @@ function AppStack() {
336338
useEffect(() => {
337339
const handleLoginSuccess = () => {
338340
setIsAuthenticated(true);
341+
triggerPostLoginTutorial();
339342
};
340343

341344
const handleLogoutSuccess = () => {

0 commit comments

Comments
 (0)