refactor: 优化创建回忆流程

- 优化CreateMemoryScreen创建回忆页面
- 优化CreateMemoryViewModel创建回忆ViewModel

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
iammm0
2026-02-10 17:10:10 +08:00
parent a29cb6ec29
commit b156fad24d
2 changed files with 107 additions and 24 deletions

View File

@@ -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()
)
}

View File

@@ -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回复可能有多条消息每条作为单独气泡显示