| .. | .. |
|---|
| 1 | 1 | import 'dart:convert'; |
|---|
| 2 | 2 | import 'dart:io'; |
|---|
| 3 | 3 | |
|---|
| 4 | +import 'package:path_provider/path_provider.dart'; |
|---|
| 5 | + |
|---|
| 4 | 6 | import 'package:flutter/material.dart'; |
|---|
| 5 | 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; |
|---|
| 6 | 8 | import 'package:go_router/go_router.dart'; |
|---|
| .. | .. |
|---|
| 302 | 304 | } |
|---|
| 303 | 305 | } |
|---|
| 304 | 306 | |
|---|
| 305 | | - void _handleIncomingVoice(Map<String, dynamic> msg) { |
|---|
| 307 | + Future<void> _handleIncomingVoice(Map<String, dynamic> msg) async { |
|---|
| 306 | 308 | final sessionId = msg['sessionId'] as String?; |
|---|
| 307 | 309 | final audioData = msg['audioBase64'] as String? ?? msg['audio'] as String? ?? msg['data'] as String?; |
|---|
| 308 | 310 | final content = msg['content'] as String? ?? msg['text'] as String? ?? ''; |
|---|
| .. | .. |
|---|
| 319 | 321 | duration: duration, |
|---|
| 320 | 322 | ); |
|---|
| 321 | 323 | |
|---|
| 324 | + // Save audio to file so it survives persistence (base64 gets stripped) |
|---|
| 325 | + String? savedAudioPath; |
|---|
| 326 | + if (audioData != null) { |
|---|
| 327 | + try { |
|---|
| 328 | + final dir = await getTemporaryDirectory(); |
|---|
| 329 | + savedAudioPath = '${dir.path}/voice_${message.id}.m4a'; |
|---|
| 330 | + final bytes = base64Decode(audioData.contains(',') ? audioData.split(',').last : audioData); |
|---|
| 331 | + await File(savedAudioPath).writeAsBytes(bytes); |
|---|
| 332 | + } catch (_) { |
|---|
| 333 | + savedAudioPath = null; |
|---|
| 334 | + } |
|---|
| 335 | + } |
|---|
| 336 | + |
|---|
| 337 | + final storedMessage = Message( |
|---|
| 338 | + id: message.id, |
|---|
| 339 | + role: message.role, |
|---|
| 340 | + type: message.type, |
|---|
| 341 | + content: content, |
|---|
| 342 | + audioUri: savedAudioPath ?? audioData, |
|---|
| 343 | + timestamp: message.timestamp, |
|---|
| 344 | + status: message.status, |
|---|
| 345 | + duration: duration, |
|---|
| 346 | + ); |
|---|
| 347 | + |
|---|
| 322 | 348 | final activeId = ref.read(activeSessionIdProvider); |
|---|
| 323 | 349 | if (sessionId != null && sessionId != activeId) { |
|---|
| 324 | | - _storeForSession(sessionId, message); |
|---|
| 350 | + _storeForSession(sessionId, storedMessage); |
|---|
| 325 | 351 | _incrementUnread(sessionId); |
|---|
| 326 | 352 | final sessions = ref.read(sessionsProvider); |
|---|
| 327 | 353 | final session = sessions.firstWhere( |
|---|
| .. | .. |
|---|
| 339 | 365 | return; |
|---|
| 340 | 366 | } |
|---|
| 341 | 367 | |
|---|
| 342 | | - ref.read(messagesProvider.notifier).addMessage(message); |
|---|
| 368 | + ref.read(messagesProvider.notifier).addMessage(storedMessage); |
|---|
| 343 | 369 | ref.read(isTypingProvider.notifier).state = false; |
|---|
| 344 | 370 | _scrollToBottom(); |
|---|
| 345 | 371 | |
|---|
| 346 | 372 | if (audioData != null && !AudioService.isBackgrounded && !_isCatchingUp && !_isRecording) { |
|---|
| 347 | | - // Queue incoming voice chunks — don't play while recording |
|---|
| 348 | 373 | AudioService.queueBase64(audioData); |
|---|
| 349 | 374 | } |
|---|
| 350 | 375 | } |
|---|