refactor: 优化创建回忆流程
- 优化CreateMemoryScreen创建回忆页面 - 优化CreateMemoryViewModel创建回忆ViewModel Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import com.huaga.life_echo.network.models.MessageDto
|
||||
import com.huaga.life_echo.ui.components.chat.ChatHeader
|
||||
import com.huaga.life_echo.ui.components.chat.ChatInputField
|
||||
import com.huaga.life_echo.ui.components.chat.MessageList
|
||||
import com.huaga.life_echo.ui.components.chat.ReleaseAction
|
||||
import com.huaga.life_echo.ui.components.chat.RecordingOverlayContent
|
||||
import com.huaga.life_echo.ui.components.chat.RecordingState
|
||||
import com.huaga.life_echo.ui.components.debug.WebSocketDebugPanel
|
||||
@@ -159,7 +160,8 @@ fun CreateMemoryScreen(
|
||||
ChatHeader(
|
||||
title = "回忆录助手",
|
||||
isOnline = connectionStatus == "已连接",
|
||||
onBackClick = { navController?.popBackStack() }
|
||||
onBackClick = { navController?.popBackStack() },
|
||||
onNewConversationClick = { viewModel.startConversation() }
|
||||
)
|
||||
|
||||
// WebSocket调试面板(仅在开发模式下显示)
|
||||
@@ -208,7 +210,8 @@ fun CreateMemoryScreen(
|
||||
keyboardController?.hide()
|
||||
}
|
||||
},
|
||||
enabled = !isStreaming && !isVoiceRecording,
|
||||
// 录音过程中也必须保持语音按钮可用,否则会丢失手指抬起事件,导致无法停止录音
|
||||
enabled = !isStreaming,
|
||||
// 语音相关回调
|
||||
onStartRecording = {
|
||||
if (hasRecordPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -226,11 +229,17 @@ fun CreateMemoryScreen(
|
||||
recordingDuration = recordingDuration
|
||||
)
|
||||
}
|
||||
// 录音浮层:仅视觉反馈,不消费触摸,松开由按钮接收并立即发送
|
||||
// 录音浮层:覆盖在聊天之上,只检测本层手势;左滑取消、右滑转文字、否则松开发送语音
|
||||
if (recordingOverlayState != RecordingState.IDLE) {
|
||||
RecordingOverlayContent(
|
||||
state = recordingOverlayState,
|
||||
duration = recordingDuration,
|
||||
onRelease = { action ->
|
||||
when (action) {
|
||||
ReleaseAction.CANCEL -> viewModel.cancelRecordingVoice()
|
||||
ReleaseAction.SEND_VOICE -> viewModel.stopAndSendRecording()
|
||||
ReleaseAction.SEND_AS_TEXT -> viewModel.stopAndSendRecordingAsText()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.network.AuthService
|
||||
import com.huaga.life_echo.network.models.MessageDto
|
||||
import com.huaga.life_echo.data.database.Chapter
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class CreateMemoryViewModel(
|
||||
private val conversationRepository: ConversationRepository,
|
||||
@@ -97,6 +99,11 @@ class CreateMemoryViewModel(
|
||||
private val _audioDurations = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
val audioDurations: StateFlow<Map<String, Int>> = _audioDurations.asStateFlow()
|
||||
|
||||
// 仅转写结果:发 transcribe_only 后等待 transcript 用
|
||||
private val pendingTranscribeChannel = Channel<String>(Channel.RENDEZVOUS)
|
||||
@Volatile
|
||||
private var waitingForTranscribeOnly = false
|
||||
|
||||
init {
|
||||
TokenManager.initialize(context)
|
||||
}
|
||||
@@ -182,6 +189,16 @@ class CreateMemoryViewModel(
|
||||
isRecording.value = true
|
||||
connectionStatus.value = "连接中..."
|
||||
|
||||
// 新建对话:清空当前会话展示状态
|
||||
historyMessages.value = emptyList()
|
||||
userMessages.value = emptyList()
|
||||
streamingText.value = ""
|
||||
agentResponse.value = ""
|
||||
isStreaming.value = false
|
||||
isTyping.value = false
|
||||
_audioFilePaths.value = emptyMap()
|
||||
_audioDurations.value = emptyMap()
|
||||
|
||||
// 清除旧的任务记录
|
||||
apiService.clearTasks()
|
||||
|
||||
@@ -244,24 +261,20 @@ class CreateMemoryViewModel(
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止录音并发送
|
||||
* 停止录音并发送(手势释放后立即调用,内部在协程中执行避免阻塞触摸回调,马上发送对应语音)
|
||||
*/
|
||||
fun stopAndSendRecording() {
|
||||
Log.d(TAG, "停止录音并发送")
|
||||
val result = voiceRecorder.stopRecording()
|
||||
|
||||
if (result == null) {
|
||||
Log.e(TAG, "录音失败,result 为空")
|
||||
return
|
||||
}
|
||||
|
||||
if (result.durationSeconds < MIN_RECORDING_DURATION) {
|
||||
Log.w(TAG, "录音时间太短: ${result.durationSeconds}s")
|
||||
// 可以在这里添加提示用户的逻辑
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "停止录音并发送")
|
||||
val result = voiceRecorder.stopRecording()
|
||||
if (result == null) {
|
||||
Log.e(TAG, "录音失败,result 为空")
|
||||
return@launch
|
||||
}
|
||||
if (result.durationSeconds < MIN_RECORDING_DURATION) {
|
||||
Log.w(TAG, "录音时间太短: ${result.durationSeconds}s")
|
||||
return@launch
|
||||
}
|
||||
sendAudioMessage(result.audioBytes, result.filePath, result.durationSeconds)
|
||||
}
|
||||
}
|
||||
@@ -274,17 +287,75 @@ class CreateMemoryViewModel(
|
||||
voiceRecorder.cancelRecording()
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止录音并转为文字发送(用于浮层「转文字」)
|
||||
*/
|
||||
fun stopAndSendRecordingAsText() {
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "停止录音并转文字发送")
|
||||
val result = voiceRecorder.stopRecording() ?: run {
|
||||
Log.e(TAG, "录音结果为空")
|
||||
return@launch
|
||||
}
|
||||
if (result.durationSeconds < MIN_RECORDING_DURATION) {
|
||||
Log.w(TAG, "录音太短: ${result.durationSeconds}s")
|
||||
return@launch
|
||||
}
|
||||
if (conversationId.value == null) {
|
||||
startConversation()
|
||||
delay(300)
|
||||
}
|
||||
if (!webSocketClient.isConnected()) {
|
||||
connectionStatus.value = "未连接,正在重连..."
|
||||
conversationId.value?.let { id ->
|
||||
val token = TokenManager.getAccessToken()
|
||||
try {
|
||||
webSocketClient.connect(
|
||||
id,
|
||||
token,
|
||||
onMessage = { m -> handleWebSocketMessage(m) },
|
||||
onError = { connectionStatus.value = "错误: $it" }
|
||||
)
|
||||
delay(300)
|
||||
} catch (e: Exception) {
|
||||
connectionStatus.value = "重连失败: ${e.message}"
|
||||
return@launch
|
||||
}
|
||||
} ?: run {
|
||||
connectionStatus.value = "对话ID为空"
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
val id = conversationId.value ?: return@launch
|
||||
try {
|
||||
waitingForTranscribeOnly = true
|
||||
webSocketClient.sendTranscribeOnly(result.audioBytes, id)
|
||||
val text = withTimeoutOrNull(15000L) { pendingTranscribeChannel.receive() }
|
||||
waitingForTranscribeOnly = false
|
||||
if (!text.isNullOrBlank() && !text.startsWith("转写失败")) {
|
||||
sendTextMessage(text)
|
||||
} else {
|
||||
connectionStatus.value = "转写失败或超时,请重试"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
waitingForTranscribeOnly = false
|
||||
Log.e(TAG, "转文字失败: ${e.message}", e)
|
||||
connectionStatus.value = "转写失败: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送音频消息
|
||||
*/
|
||||
private suspend fun sendAudioMessage(audioBytes: ByteArray, filePath: String, durationSeconds: Int) {
|
||||
Log.d(TAG, "准备发送音频消息,大小: ${audioBytes.size}, 时长: ${durationSeconds}s")
|
||||
|
||||
// 确保已连接
|
||||
// 确保已连接(手势松开后马上发语音,尽量缩短等待)
|
||||
if (conversationId.value == null) {
|
||||
Log.d(TAG, "对话ID为空,开始创建新对话")
|
||||
startConversation()
|
||||
delay(500)
|
||||
delay(300)
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
@@ -301,7 +372,7 @@ class CreateMemoryViewModel(
|
||||
onMessage = { message -> handleWebSocketMessage(message) },
|
||||
onError = { errorMsg -> connectionStatus.value = "错误: $errorMsg" }
|
||||
)
|
||||
delay(500)
|
||||
delay(300)
|
||||
} catch (e: Exception) {
|
||||
connectionStatus.value = "重连失败: ${e.message}"
|
||||
return
|
||||
@@ -311,7 +382,7 @@ class CreateMemoryViewModel(
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
conversationId.value?.let { id ->
|
||||
// 生成临时消息 ID
|
||||
val tempMessageId = "audio_user_${System.currentTimeMillis()}"
|
||||
@@ -461,10 +532,13 @@ class CreateMemoryViewModel(
|
||||
|
||||
when (message.type) {
|
||||
MessageType.transcript -> {
|
||||
// 语音转文字结果
|
||||
val text = message.getString("text") ?: ""
|
||||
transcript.value = text
|
||||
Log.d(TAG, "收到语音转文字结果: $text")
|
||||
if (waitingForTranscribeOnly) {
|
||||
waitingForTranscribeOnly = false
|
||||
pendingTranscribeChannel.trySend(text)
|
||||
}
|
||||
}
|
||||
MessageType.agent_response -> {
|
||||
// 处理Agent回复(可能有多条消息,每条作为单独气泡显示)
|
||||
|
||||
Reference in New Issue
Block a user