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.ChatHeader
|
||||||
import com.huaga.life_echo.ui.components.chat.ChatInputField
|
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.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.RecordingOverlayContent
|
||||||
import com.huaga.life_echo.ui.components.chat.RecordingState
|
import com.huaga.life_echo.ui.components.chat.RecordingState
|
||||||
import com.huaga.life_echo.ui.components.debug.WebSocketDebugPanel
|
import com.huaga.life_echo.ui.components.debug.WebSocketDebugPanel
|
||||||
@@ -159,7 +160,8 @@ fun CreateMemoryScreen(
|
|||||||
ChatHeader(
|
ChatHeader(
|
||||||
title = "回忆录助手",
|
title = "回忆录助手",
|
||||||
isOnline = connectionStatus == "已连接",
|
isOnline = connectionStatus == "已连接",
|
||||||
onBackClick = { navController?.popBackStack() }
|
onBackClick = { navController?.popBackStack() },
|
||||||
|
onNewConversationClick = { viewModel.startConversation() }
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebSocket调试面板(仅在开发模式下显示)
|
// WebSocket调试面板(仅在开发模式下显示)
|
||||||
@@ -208,7 +210,8 @@ fun CreateMemoryScreen(
|
|||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = !isStreaming && !isVoiceRecording,
|
// 录音过程中也必须保持语音按钮可用,否则会丢失手指抬起事件,导致无法停止录音
|
||||||
|
enabled = !isStreaming,
|
||||||
// 语音相关回调
|
// 语音相关回调
|
||||||
onStartRecording = {
|
onStartRecording = {
|
||||||
if (hasRecordPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (hasRecordPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -226,11 +229,17 @@ fun CreateMemoryScreen(
|
|||||||
recordingDuration = recordingDuration
|
recordingDuration = recordingDuration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 录音浮层:仅视觉反馈,不消费触摸,松开由按钮接收并立即发送
|
// 录音浮层:覆盖在聊天之上,只检测本层手势;左滑取消、右滑转文字、否则松开发送语音
|
||||||
if (recordingOverlayState != RecordingState.IDLE) {
|
if (recordingOverlayState != RecordingState.IDLE) {
|
||||||
RecordingOverlayContent(
|
RecordingOverlayContent(
|
||||||
state = recordingOverlayState,
|
|
||||||
duration = recordingDuration,
|
duration = recordingDuration,
|
||||||
|
onRelease = { action ->
|
||||||
|
when (action) {
|
||||||
|
ReleaseAction.CANCEL -> viewModel.cancelRecordingVoice()
|
||||||
|
ReleaseAction.SEND_VOICE -> viewModel.stopAndSendRecording()
|
||||||
|
ReleaseAction.SEND_AS_TEXT -> viewModel.stopAndSendRecordingAsText()
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
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.AuthService
|
||||||
import com.huaga.life_echo.network.models.MessageDto
|
import com.huaga.life_echo.network.models.MessageDto
|
||||||
import com.huaga.life_echo.data.database.Chapter
|
import com.huaga.life_echo.data.database.Chapter
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
class CreateMemoryViewModel(
|
class CreateMemoryViewModel(
|
||||||
private val conversationRepository: ConversationRepository,
|
private val conversationRepository: ConversationRepository,
|
||||||
@@ -97,6 +99,11 @@ class CreateMemoryViewModel(
|
|||||||
private val _audioDurations = MutableStateFlow<Map<String, Int>>(emptyMap())
|
private val _audioDurations = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
val audioDurations: StateFlow<Map<String, Int>> = _audioDurations.asStateFlow()
|
val audioDurations: StateFlow<Map<String, Int>> = _audioDurations.asStateFlow()
|
||||||
|
|
||||||
|
// 仅转写结果:发 transcribe_only 后等待 transcript 用
|
||||||
|
private val pendingTranscribeChannel = Channel<String>(Channel.RENDEZVOUS)
|
||||||
|
@Volatile
|
||||||
|
private var waitingForTranscribeOnly = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
TokenManager.initialize(context)
|
TokenManager.initialize(context)
|
||||||
}
|
}
|
||||||
@@ -182,6 +189,16 @@ class CreateMemoryViewModel(
|
|||||||
isRecording.value = true
|
isRecording.value = true
|
||||||
connectionStatus.value = "连接中..."
|
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()
|
apiService.clearTasks()
|
||||||
|
|
||||||
@@ -244,24 +261,20 @@ class CreateMemoryViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止录音并发送
|
* 停止录音并发送(手势释放后立即调用,内部在协程中执行避免阻塞触摸回调,马上发送对应语音)
|
||||||
*/
|
*/
|
||||||
fun stopAndSendRecording() {
|
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 {
|
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)
|
sendAudioMessage(result.audioBytes, result.filePath, result.durationSeconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,17 +287,75 @@ class CreateMemoryViewModel(
|
|||||||
voiceRecorder.cancelRecording()
|
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) {
|
private suspend fun sendAudioMessage(audioBytes: ByteArray, filePath: String, durationSeconds: Int) {
|
||||||
Log.d(TAG, "准备发送音频消息,大小: ${audioBytes.size}, 时长: ${durationSeconds}s")
|
Log.d(TAG, "准备发送音频消息,大小: ${audioBytes.size}, 时长: ${durationSeconds}s")
|
||||||
|
|
||||||
// 确保已连接
|
// 确保已连接(手势松开后马上发语音,尽量缩短等待)
|
||||||
if (conversationId.value == null) {
|
if (conversationId.value == null) {
|
||||||
Log.d(TAG, "对话ID为空,开始创建新对话")
|
Log.d(TAG, "对话ID为空,开始创建新对话")
|
||||||
startConversation()
|
startConversation()
|
||||||
delay(500)
|
delay(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查连接状态
|
// 检查连接状态
|
||||||
@@ -301,7 +372,7 @@ class CreateMemoryViewModel(
|
|||||||
onMessage = { message -> handleWebSocketMessage(message) },
|
onMessage = { message -> handleWebSocketMessage(message) },
|
||||||
onError = { errorMsg -> connectionStatus.value = "错误: $errorMsg" }
|
onError = { errorMsg -> connectionStatus.value = "错误: $errorMsg" }
|
||||||
)
|
)
|
||||||
delay(500)
|
delay(300)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
connectionStatus.value = "重连失败: ${e.message}"
|
connectionStatus.value = "重连失败: ${e.message}"
|
||||||
return
|
return
|
||||||
@@ -461,10 +532,13 @@ class CreateMemoryViewModel(
|
|||||||
|
|
||||||
when (message.type) {
|
when (message.type) {
|
||||||
MessageType.transcript -> {
|
MessageType.transcript -> {
|
||||||
// 语音转文字结果
|
|
||||||
val text = message.getString("text") ?: ""
|
val text = message.getString("text") ?: ""
|
||||||
transcript.value = text
|
transcript.value = text
|
||||||
Log.d(TAG, "收到语音转文字结果: $text")
|
Log.d(TAG, "收到语音转文字结果: $text")
|
||||||
|
if (waitingForTranscribeOnly) {
|
||||||
|
waitingForTranscribeOnly = false
|
||||||
|
pendingTranscribeChannel.trySend(text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MessageType.agent_response -> {
|
MessageType.agent_response -> {
|
||||||
// 处理Agent回复(可能有多条消息,每条作为单独气泡显示)
|
// 处理Agent回复(可能有多条消息,每条作为单独气泡显示)
|
||||||
|
|||||||
Reference in New Issue
Block a user