diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue
index a3bb190..8e18868 100644
--- a/frontend/src/views/ChatView.vue
+++ b/frontend/src/views/ChatView.vue
@@ -337,7 +337,7 @@
viewportWidth.value < 768)
+ const LOCAL_HISTORY_KEY = 'pet_chat_history_backup'
const petAvatarStyle = computed(() => {
const x = Number(petPosition.value.x) || 50
@@ -978,9 +984,40 @@ export default {
await switchToTextMode()
}
- const resetToDefaultMode = async () => {
+ const forceStopVoiceAndCall = async () => {
+ try {
+ if (isRecording.value) {
+ await chatStore.stopRecording({ sendEndSpeech: false })
+ }
+ } catch (error) {
+ console.warn('[ChatView] stopRecording while switching to text failed:', error)
+ }
+
+ try {
+ chatStore.stopAudioPlayback()
+ } catch (error) {
+ console.warn('[ChatView] stopAudioPlayback while switching to text failed:', error)
+ }
+
+ try {
+ chatStore.exitVoiceChatMode()
+ } catch (error) {
+ console.warn('[ChatView] exitVoiceChatMode while switching to text failed:', error)
+ }
+
+ try {
+ chatStore.exitRealtimeCallMode()
+ } catch (error) {
+ console.warn('[ChatView] exitRealtimeCallMode while switching to text failed:', error)
+ }
+
isInCall.value = false
isRecording.value = false
+ stopCallTimer()
+ }
+
+ const resetToDefaultMode = async () => {
+ await forceStopVoiceAndCall()
showInputPanel.value = false
chatMode.value = 'none'
isAiTyping.value = false
@@ -991,8 +1028,7 @@ export default {
}
const switchToTextMode = async () => {
- isInCall.value = false
- isRecording.value = false
+ await forceStopVoiceAndCall()
showInputPanel.value = true
chatMode.value = 'text'
@@ -1205,6 +1241,78 @@ export default {
}
}
+ const getSavedHistoryBackup = () => {
+ try {
+ const saved = localStorage.getItem(LOCAL_HISTORY_KEY)
+ return saved ? JSON.parse(saved) : {}
+ } catch (err) {
+ console.error('Failed to load chat history backup:', err)
+ return {}
+ }
+ }
+
+ const saveHistoryBackup = (message, mode = 'text') => {
+ try {
+ if (!message?.content) return
+ const petId = activePetInChat.value?.pet?.id || currentPet.value?.id || null
+ const backgroundId = currentBackground.value?.id || null
+ const backup = getSavedHistoryBackup()
+ const key = `${petId || 'unknown'}_${backgroundId || 'unknown'}`
+ const list = Array.isArray(backup[key]) ? backup[key] : []
+
+ const lastItem = list[list.length - 1]
+ const now = new Date().toISOString()
+ if (mode === 'voice' || mode === 'realtime') {
+ if (message.isUser) {
+ list.push({
+ id: message.id || now,
+ user_msg: message.content,
+ ai_msg: '',
+ created_at: message.timestamp || now,
+ mode
+ })
+ } else if (lastItem && !lastItem.ai_msg) {
+ lastItem.ai_msg = message.content
+ lastItem.updated_at = message.timestamp || now
+ } else {
+ list.push({
+ id: message.id || now,
+ user_msg: '',
+ ai_msg: message.content,
+ created_at: message.timestamp || now,
+ mode
+ })
+ }
+ } else {
+ if (message.isUser) {
+ list.push({
+ id: message.id || now,
+ user_msg: message.content,
+ ai_msg: '',
+ created_at: message.timestamp || now,
+ mode
+ })
+ } else if (lastItem && !lastItem.ai_msg) {
+ lastItem.ai_msg = message.content
+ lastItem.updated_at = message.timestamp || now
+ } else {
+ list.push({
+ id: message.id || now,
+ user_msg: '',
+ ai_msg: message.content,
+ created_at: message.timestamp || now,
+ mode
+ })
+ }
+ }
+
+ backup[key] = list.slice(-200)
+ localStorage.setItem(LOCAL_HISTORY_KEY, JSON.stringify(backup))
+ } catch (err) {
+ console.error('Failed to save chat history backup:', err)
+ }
+ }
+
const getSavedConfig = () => {
try {
const saved = localStorage.getItem(CONFIG_STORAGE_KEY)
@@ -1383,7 +1491,15 @@ export default {
const backgroundId = currentBackground.value?.id
console.log('[ChatView] Sending message:', messageText, 'petId:', petId, 'backgroundId:', backgroundId)
- await chatStore.sendMessage(messageText, { petId, backgroundId })
+ const response = await chatStore.sendMessage(messageText, { petId, backgroundId })
+ const userMsg = chatStore.messages.find(m => m.isUser && m.content === messageText && m.status === 'sent')
+ if (userMsg) {
+ saveHistoryBackup(userMsg, 'text')
+ }
+ const aiMsg = [...chatStore.messages].reverse().find(m => !m.isUser && m.status === 'completed')
+ if (aiMsg) {
+ saveHistoryBackup(aiMsg, 'text')
+ }
console.log('[ChatView] Message sent successfully, messages:', chatStore.messages)
console.log('[ChatView] latestAiMessage:', latestAiMessage.value)
scrollToBottom()
@@ -1398,17 +1514,13 @@ export default {
}
const toggleVoiceInput = async () => {
- if (isRecording.value) {
- await stopRecording()
- }
if (chatMode.value === 'voice') {
- chatStore.exitVoiceChatMode()
await resetToDefaultMode()
} else {
try {
const hasPermission = await chatStore.requestMicrophonePermission()
if (hasPermission) {
- chatStore.exitVoiceChatMode()
+ await forceStopVoiceAndCall()
showInputPanel.value = true
chatMode.value = 'voice'
}
@@ -1422,6 +1534,7 @@ export default {
const startVoiceRecording = async () => {
if (isRecording.value) return
try {
+ await forceStopVoiceAndCall()
if (!chatStore.isVoiceConnected) {
await chatStore.connectVoiceWebSocket()
await new Promise(resolve => setTimeout(resolve, 300))
@@ -1486,8 +1599,7 @@ export default {
const toggleCallMode = async () => {
if (isInCall.value || chatMode.value === 'call') {
await stopCallRecording()
- chatStore.exitRealtimeCallMode()
- stopCallTimer()
+ await forceStopVoiceAndCall()
await resetToDefaultMode()
return
}
@@ -1498,7 +1610,7 @@ export default {
return
}
- chatStore.exitVoiceChatMode()
+ await forceStopVoiceAndCall()
chatMode.value = 'call'
showInputPanel.value = true
@@ -1686,7 +1798,34 @@ export default {
}
const loadHistoryConversation = (item) => {
- chatStore.clearMessages()
+ if (item?.user_msg || item?.ai_msg) {
+ const historyMessages = [
+ {
+ id: `history-user-${item.id || Date.now()}`,
+ content: item.user_msg || '',
+ isUser: true,
+ timestamp: item.created_at,
+ status: 'completed',
+ source: 'history'
+ }
+ ]
+
+ if (item.ai_msg) {
+ historyMessages.push({
+ id: `history-ai-${item.id || Date.now()}`,
+ content: item.ai_msg,
+ isUser: false,
+ timestamp: item.created_at,
+ status: 'completed',
+ source: 'history'
+ })
+ }
+
+ chatStore.messages = historyMessages
+ } else {
+ chatStore.clearMessages()
+ }
+
showHistoryPanel.value = false
resetToDefaultMode()
}
@@ -1747,9 +1886,19 @@ export default {
console.log('[ChatView] New messages:', newMessages)
console.log('[ChatView] Old messages length:', oldMessages?.length)
- // 找出最新的AI消息
const latestAi = newMessages?.slice().reverse().find(m => !m.isUser)
console.log('[ChatView] Latest AI message found:', latestAi)
+
+ if (Array.isArray(newMessages) && newMessages.length) {
+ newMessages.forEach((message) => {
+ if (!message?.id || savedHistoryMessageKeys.has(message.id)) return
+ if (message.source === 'history') return
+ if (message.status === 'streaming') return
+ const mode = chatMode.value === 'call' ? 'realtime' : chatMode.value === 'voice' ? 'voice' : 'text'
+ saveHistoryBackup(message, mode)
+ savedHistoryMessageKeys.add(message.id)
+ })
+ }
syncStoreState()
scrollToBottom()
@@ -3174,6 +3323,16 @@ export default {
overflow: hidden;
}
+ .history-date-picker {
+ width: 100%;
+ min-width: 240px;
+ flex: 1 1 260px;
+ }
+
+ .history-filters {
+ align-items: center;
+ }
+
.el-dialog__header {
background: transparent !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
@@ -3216,6 +3375,84 @@ export default {
.el-dialog__headerbtn:hover .el-dialog__close {
color: #fff;
}
+
+ @media (max-width: 768px) {
+ .el-dialog {
+ width: calc(100vw - 16px) !important;
+ max-height: calc(100vh - 16px);
+ margin: 8px auto !important;
+ border-radius: 14px;
+ }
+
+ .el-dialog__header {
+ padding: 12px 14px;
+ }
+
+ .el-dialog__title {
+ font-size: 16px;
+ }
+
+ .el-dialog__body {
+ padding: 12px 14px 14px;
+ }
+
+ .history-filters {
+ gap: 8px;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .history-list {
+ max-height: calc(100vh - 300px);
+ overflow-y: auto;
+ }
+
+ .history-pagination {
+ margin-top: 12px;
+ flex-shrink: 0;
+ }
+
+ .history-date-picker,
+ :deep(.history-dialog .el-select),
+ .history-context-tag {
+ width: 100%;
+ min-width: 0;
+ }
+
+ .history-date-picker {
+ :deep(.el-range-editor),
+ :deep(.el-input__wrapper) {
+ width: 100%;
+ }
+ }
+
+ :deep(.history-dialog .el-date-range-picker) {
+ width: calc(100vw - 16px) !important;
+ max-width: calc(100vw - 16px) !important;
+ }
+
+ :deep(.history-dialog .el-picker-panel) {
+ width: 100% !important;
+ max-width: 100% !important;
+ }
+
+ :deep(.history-dialog .el-date-range-picker__content) {
+ width: 100%;
+ float: none;
+ }
+
+ :deep(.history-dialog .el-date-range-picker__header) {
+ padding: 8px 0;
+ }
+
+ :deep(.history-dialog .el-date-table) {
+ width: 100%;
+ }
+
+ .history-conversation-item .message-item .message-content {
+ max-width: 88%;
+ }
+ }
}
.feature-tooltip {