mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 01:53:53 +08:00
Merge pull request #3085 from xinnan-tech/update-mobile-chat-history
mobile 角色配置添加小参数模型选项,历史记录标题优化,聊天详情参数记录折叠处理
This commit is contained in:
@@ -28,6 +28,7 @@ export interface AgentDetail {
|
|||||||
asrModelId: string
|
asrModelId: string
|
||||||
vadModelId: string
|
vadModelId: string
|
||||||
llmModelId: string
|
llmModelId: string
|
||||||
|
slmModelId: string
|
||||||
vllmModelId: string
|
vllmModelId: string
|
||||||
ttsModelId: string
|
ttsModelId: string
|
||||||
ttsVoiceId: string
|
ttsVoiceId: string
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export function getChatHistory(agentId: string, sessionId: string) {
|
|||||||
ignoreAuth: false,
|
ignoreAuth: false,
|
||||||
toast: false,
|
toast: false,
|
||||||
},
|
},
|
||||||
|
cacheFor: {
|
||||||
|
expire: -1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface ChatSession {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
chatCount: number
|
chatCount: number
|
||||||
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 聊天会话列表响应
|
// 聊天会话列表响应
|
||||||
@@ -14,7 +15,7 @@ export interface ChatSessionsResponse {
|
|||||||
// 聊天消息
|
// 聊天消息
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
createdAt: string
|
createdAt: string
|
||||||
chatType: 1 | 2 // 1是用户,2是AI
|
chatType: 1 | 2 | 3 // 1是用户,2是AI,3是参数说明
|
||||||
content: string
|
content: string
|
||||||
audioId: string | null
|
audioId: string | null
|
||||||
macAddress: string
|
macAddress: string
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export default {
|
|||||||
'agent.modelConfig': 'Modellkonfiguration',
|
'agent.modelConfig': 'Modellkonfiguration',
|
||||||
'agent.vad': 'Sprachaktivitätserkennung',
|
'agent.vad': 'Sprachaktivitätserkennung',
|
||||||
'agent.asr': 'Spracherkennung',
|
'agent.asr': 'Spracherkennung',
|
||||||
'agent.llm': 'Großes Sprachmodell',
|
'agent.llm': 'Hauptsprachenmodell',
|
||||||
|
'agent.slm': 'Kleine Parametermodelle',
|
||||||
'agent.vllm': 'Vision-Sprachmodell',
|
'agent.vllm': 'Vision-Sprachmodell',
|
||||||
'agent.intent': 'Absichtserkennung',
|
'agent.intent': 'Absichtserkennung',
|
||||||
'agent.memory': 'Speicher',
|
'agent.memory': 'Speicher',
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export default {
|
|||||||
'agent.modelConfig': 'Model Configuration',
|
'agent.modelConfig': 'Model Configuration',
|
||||||
'agent.vad': 'Voice Activity Detection',
|
'agent.vad': 'Voice Activity Detection',
|
||||||
'agent.asr': 'Speech Recognition',
|
'agent.asr': 'Speech Recognition',
|
||||||
'agent.llm': 'Large Language Model',
|
'agent.llm': 'Main language model',
|
||||||
|
'agent.slm': 'Small parameter model',
|
||||||
'agent.vllm': 'Vision Language Model',
|
'agent.vllm': 'Vision Language Model',
|
||||||
'agent.intent': 'Intent Recognition',
|
'agent.intent': 'Intent Recognition',
|
||||||
'agent.memory': 'Memory',
|
'agent.memory': 'Memory',
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export default {
|
|||||||
'agent.modelConfig': 'Configuração do Modelo',
|
'agent.modelConfig': 'Configuração do Modelo',
|
||||||
'agent.vad': 'Detecção de Atividade de Voz',
|
'agent.vad': 'Detecção de Atividade de Voz',
|
||||||
'agent.asr': 'Reconhecimento de Fala',
|
'agent.asr': 'Reconhecimento de Fala',
|
||||||
'agent.llm': 'Modelo de Linguagem Grande',
|
'agent.llm': 'Modelo de Linguagem Principal',
|
||||||
|
'agent.slm': 'Modelo de pequenos parâmetros',
|
||||||
'agent.vllm': 'Modelo de Linguagem Visual',
|
'agent.vllm': 'Modelo de Linguagem Visual',
|
||||||
'agent.intent': 'Reconhecimento de Intenção',
|
'agent.intent': 'Reconhecimento de Intenção',
|
||||||
'agent.memory': 'Memória',
|
'agent.memory': 'Memória',
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export default {
|
|||||||
'agent.modelConfig': 'Cấu hình mô hình',
|
'agent.modelConfig': 'Cấu hình mô hình',
|
||||||
'agent.vad': 'Phát hiện hoạt động giọng nói',
|
'agent.vad': 'Phát hiện hoạt động giọng nói',
|
||||||
'agent.asr': 'Nhận dạng giọng nói',
|
'agent.asr': 'Nhận dạng giọng nói',
|
||||||
'agent.llm': 'Mô hình ngôn ngữ lớn',
|
'agent.llm': 'Mô hình ngôn ngữ chính',
|
||||||
|
'agent.slm': 'Mô hình tham số nhỏ',
|
||||||
'agent.vllm': 'Mô hình ngôn ngữ thị giác',
|
'agent.vllm': 'Mô hình ngôn ngữ thị giác',
|
||||||
'agent.intent': 'Nhận dạng ý định',
|
'agent.intent': 'Nhận dạng ý định',
|
||||||
'agent.memory': 'Bộ nhớ',
|
'agent.memory': 'Bộ nhớ',
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export default {
|
|||||||
'agent.modelConfig': '模型配置',
|
'agent.modelConfig': '模型配置',
|
||||||
'agent.vad': '语音活动检测',
|
'agent.vad': '语音活动检测',
|
||||||
'agent.asr': '语音识别',
|
'agent.asr': '语音识别',
|
||||||
'agent.llm': '大语言模型',
|
'agent.llm': '主语言模型',
|
||||||
|
'agent.slm': '小参数模型',
|
||||||
'agent.vllm': '视觉大模型',
|
'agent.vllm': '视觉大模型',
|
||||||
'agent.intent': '意图识别',
|
'agent.intent': '意图识别',
|
||||||
'agent.memory': '记忆',
|
'agent.memory': '记忆',
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ export default {
|
|||||||
'agent.modelConfig': '模型配置',
|
'agent.modelConfig': '模型配置',
|
||||||
'agent.vad': '語音活動檢測',
|
'agent.vad': '語音活動檢測',
|
||||||
'agent.asr': '語音識別',
|
'agent.asr': '語音識別',
|
||||||
'agent.llm': '大語言模型',
|
'agent.llm': '主語言模型',
|
||||||
|
'agent.slm': '小參數模型',
|
||||||
'agent.vllm': '視覺大模型',
|
'agent.vllm': '視覺大模型',
|
||||||
'agent.intent': '意圖識別',
|
'agent.intent': '意圖識別',
|
||||||
'agent.memory': '記憶',
|
'agent.memory': '記憶',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const formData = ref<Partial<AgentDetail>>({
|
|||||||
vadModelId: '',
|
vadModelId: '',
|
||||||
asrModelId: '',
|
asrModelId: '',
|
||||||
llmModelId: '',
|
llmModelId: '',
|
||||||
|
slmModelId: '',
|
||||||
vllmModelId: '',
|
vllmModelId: '',
|
||||||
intentModelId: '',
|
intentModelId: '',
|
||||||
memModelId: '',
|
memModelId: '',
|
||||||
@@ -46,6 +47,7 @@ const displayNames = ref({
|
|||||||
vad: t('agent.pleaseSelect'),
|
vad: t('agent.pleaseSelect'),
|
||||||
asr: t('agent.pleaseSelect'),
|
asr: t('agent.pleaseSelect'),
|
||||||
llm: t('agent.pleaseSelect'),
|
llm: t('agent.pleaseSelect'),
|
||||||
|
slm: t('agent.pleaseSelect'),
|
||||||
vllm: t('agent.pleaseSelect'),
|
vllm: t('agent.pleaseSelect'),
|
||||||
intent: t('agent.pleaseSelect'),
|
intent: t('agent.pleaseSelect'),
|
||||||
memory: t('agent.pleaseSelect'),
|
memory: t('agent.pleaseSelect'),
|
||||||
@@ -94,6 +96,7 @@ const pickerShow = ref<{
|
|||||||
vad: false,
|
vad: false,
|
||||||
asr: false,
|
asr: false,
|
||||||
llm: false,
|
llm: false,
|
||||||
|
slm: false,
|
||||||
vllm: false,
|
vllm: false,
|
||||||
intent: false,
|
intent: false,
|
||||||
memory: false,
|
memory: false,
|
||||||
@@ -272,6 +275,7 @@ function updateDisplayNames() {
|
|||||||
displayNames.value.vad = getModelDisplayName('VAD', formData.value.vadModelId)
|
displayNames.value.vad = getModelDisplayName('VAD', formData.value.vadModelId)
|
||||||
displayNames.value.asr = getModelDisplayName('ASR', formData.value.asrModelId)
|
displayNames.value.asr = getModelDisplayName('ASR', formData.value.asrModelId)
|
||||||
displayNames.value.llm = getModelDisplayName('LLM', formData.value.llmModelId)
|
displayNames.value.llm = getModelDisplayName('LLM', formData.value.llmModelId)
|
||||||
|
displayNames.value.slm = getModelDisplayName('LLM', formData.value.slmModelId)
|
||||||
displayNames.value.vllm = getModelDisplayName('VLLM', formData.value.vllmModelId)
|
displayNames.value.vllm = getModelDisplayName('VLLM', formData.value.vllmModelId)
|
||||||
displayNames.value.intent = getModelDisplayName('Intent', formData.value.intentModelId)
|
displayNames.value.intent = getModelDisplayName('Intent', formData.value.intentModelId)
|
||||||
displayNames.value.memory = getModelDisplayName('Memory', formData.value.memModelId)
|
displayNames.value.memory = getModelDisplayName('Memory', formData.value.memModelId)
|
||||||
@@ -279,7 +283,6 @@ function updateDisplayNames() {
|
|||||||
|
|
||||||
// 角色音色特殊处理
|
// 角色音色特殊处理
|
||||||
displayNames.value.report = reportOptions.find(item => item.value === formData.value.chatHistoryConf)?.name
|
displayNames.value.report = reportOptions.find(item => item.value === formData.value.chatHistoryConf)?.name
|
||||||
displayNames.value.language = formData.value.ttsLanguage
|
|
||||||
|
|
||||||
isVisibleReport.value = formData.value.memModelId !== 'Memory_nomem'
|
isVisibleReport.value = formData.value.memModelId !== 'Memory_nomem'
|
||||||
|
|
||||||
@@ -437,6 +440,7 @@ function selectRoleTemplate(templateId: string) {
|
|||||||
vadModelId: template.vadModelId || formData.value.vadModelId,
|
vadModelId: template.vadModelId || formData.value.vadModelId,
|
||||||
asrModelId: template.asrModelId || formData.value.asrModelId,
|
asrModelId: template.asrModelId || formData.value.asrModelId,
|
||||||
llmModelId: template.llmModelId || formData.value.llmModelId,
|
llmModelId: template.llmModelId || formData.value.llmModelId,
|
||||||
|
slmModelId: template.llmModelId || formData.value.slmModelId,
|
||||||
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
|
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
|
||||||
intentModelId: template.intentModelId || formData.value.intentModelId,
|
intentModelId: template.intentModelId || formData.value.intentModelId,
|
||||||
memModelId: template.memModelId || formData.value.memModelId,
|
memModelId: template.memModelId || formData.value.memModelId,
|
||||||
@@ -474,6 +478,9 @@ async function onPickerConfirm(type: string, value: any, name: string) {
|
|||||||
case 'llm':
|
case 'llm':
|
||||||
formData.value.llmModelId = value
|
formData.value.llmModelId = value
|
||||||
break
|
break
|
||||||
|
case 'slm':
|
||||||
|
formData.value.slmModelId = value
|
||||||
|
break
|
||||||
case 'vllm':
|
case 'vllm':
|
||||||
formData.value.vllmModelId = value
|
formData.value.vllmModelId = value
|
||||||
break
|
break
|
||||||
@@ -490,7 +497,8 @@ async function onPickerConfirm(type: string, value: any, name: string) {
|
|||||||
if (value === 'Memory_nomem' || value === 'Memory_mem_report_only') {
|
if (value === 'Memory_nomem' || value === 'Memory_mem_report_only') {
|
||||||
tempSummaryMemory.value = formData.value.summaryMemory
|
tempSummaryMemory.value = formData.value.summaryMemory
|
||||||
formData.value.summaryMemory = ''
|
formData.value.summaryMemory = ''
|
||||||
} else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
|
}
|
||||||
|
else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
|
||||||
formData.value.summaryMemory = tempSummaryMemory.value
|
formData.value.summaryMemory = tempSummaryMemory.value
|
||||||
tempSummaryMemory.value = ''
|
tempSummaryMemory.value = ''
|
||||||
}
|
}
|
||||||
@@ -839,6 +847,16 @@ onMounted(async () => {
|
|||||||
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
|
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('slm')">
|
||||||
|
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||||
|
{{ t('agent.slm') }}
|
||||||
|
</text>
|
||||||
|
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
|
||||||
|
{{ displayNames.slm }}
|
||||||
|
</text>
|
||||||
|
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
|
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
|
||||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||||
{{ t('agent.vllm') }}
|
{{ t('agent.vllm') }}
|
||||||
@@ -995,6 +1013,13 @@ onMounted(async () => {
|
|||||||
@select="({ item }) => onPickerConfirm('llm', item.value, item.name)"
|
@select="({ item }) => onPickerConfirm('llm', item.value, item.name)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<wd-action-sheet
|
||||||
|
v-model="pickerShow.slm"
|
||||||
|
:actions="modelOptions.LLM && modelOptions.LLM.map(item => ({ name: item.modelName, value: item.id }))"
|
||||||
|
@close="onPickerCancel('slm')"
|
||||||
|
@select="({ item }) => onPickerConfirm('slm', item.value, item.name)"
|
||||||
|
/>
|
||||||
|
|
||||||
<wd-action-sheet
|
<wd-action-sheet
|
||||||
v-model="pickerShow.vllm"
|
v-model="pickerShow.vllm"
|
||||||
:actions="modelOptions.VLLM && modelOptions.VLLM.map(item => ({ name: item.modelName, value: item.id }))"
|
:actions="modelOptions.VLLM && modelOptions.VLLM.map(item => ({ name: item.modelName, value: item.id }))"
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const loading = ref(false)
|
|||||||
// 音频播放相关
|
// 音频播放相关
|
||||||
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
|
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
|
||||||
const playingAudioId = ref<string | null>(null)
|
const playingAudioId = ref<string | null>(null)
|
||||||
|
const expandedToolResults = ref({})
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
function goBack() {
|
function goBack() {
|
||||||
@@ -124,8 +125,50 @@ function getSpeakerName(message: ChatMessage): string {
|
|||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
function formatTime(timeStr: string) {
|
function formatTime(timeStr: string) {
|
||||||
const date = new Date(timeStr)
|
if (!timeStr)
|
||||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
return t('chatHistory.unknownTime')
|
||||||
|
|
||||||
|
// 处理时间字符串,确保格式正确
|
||||||
|
const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return timeStr // 如果解析失败,直接返回原字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
|
||||||
|
// 小于1分钟
|
||||||
|
if (diff < 60000)
|
||||||
|
return t('chatHistory.justNow')
|
||||||
|
|
||||||
|
// 小于1小时
|
||||||
|
if (diff < 3600000)
|
||||||
|
return t('chatHistory.minutesAgo', { minutes: Math.floor(diff / 60000) })
|
||||||
|
|
||||||
|
// 小于1天(24小时)
|
||||||
|
if (diff < 86400000)
|
||||||
|
return t('chatHistory.hoursAgo', { hours: Math.floor(diff / 3600000) })
|
||||||
|
|
||||||
|
// 小于7天
|
||||||
|
if (diff < 604800000) {
|
||||||
|
const days = Math.floor(diff / 86400000)
|
||||||
|
return t('chatHistory.daysAgo', { days })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超过7天,显示具体日期
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const currentYear = now.getFullYear()
|
||||||
|
|
||||||
|
// 如果是当前年份,不显示年份
|
||||||
|
if (year === currentYear) {
|
||||||
|
return `${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放音频
|
// 播放音频
|
||||||
@@ -192,11 +235,56 @@ const playAudio = debounce(async (audioId: string) => {
|
|||||||
}
|
}
|
||||||
}, 400)
|
}, 400)
|
||||||
|
|
||||||
|
function extractContentFromString(content: string) {
|
||||||
|
if (!content || content.trim() === '') {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析为 JSON
|
||||||
|
try {
|
||||||
|
const jsonObj = JSON.parse(content)
|
||||||
|
|
||||||
|
// 如果是数组格式(包含 text 和 tool)
|
||||||
|
if (Array.isArray(jsonObj)) {
|
||||||
|
return jsonObj
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是对象且有 content 字段
|
||||||
|
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
|
||||||
|
return jsonObj.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// 如果不是有效的 JSON,直接返回原内容
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleToolResult(messageIndex, itemIndex) {
|
||||||
|
const key = `${messageIndex}-${itemIndex}`
|
||||||
|
expandedToolResults.value[key] = !expandedToolResults.value[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToolResultCollapsed(messageIndex, itemIndex) {
|
||||||
|
const key = `${messageIndex}-${itemIndex}`
|
||||||
|
// 默认折叠(true表示折叠)
|
||||||
|
return !expandedToolResults.value[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstLineText(text: string) {
|
||||||
|
if (!text) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const firstLine = text.split('\n')[0]
|
||||||
|
return firstLine.length < text.length ? `${firstLine}...` : text
|
||||||
|
}
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options?.sessionId && options?.agentId) {
|
if (options?.sessionId && options?.agentId) {
|
||||||
sessionId.value = options.sessionId
|
sessionId.value = options.sessionId
|
||||||
agentId.value = options.agentId
|
agentId.value = options.agentId
|
||||||
loadChatHistory()
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error('缺少必要参数')
|
console.error('缺少必要参数')
|
||||||
@@ -204,6 +292,10 @@ onLoad((options) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
loadChatHistory()
|
||||||
|
})
|
||||||
|
|
||||||
// 页面销毁时清理音频资源
|
// 页面销毁时清理音频资源
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
if (audioContext.value) {
|
if (audioContext.value) {
|
||||||
@@ -256,6 +348,7 @@ onUnload(() => {
|
|||||||
:class="{
|
:class="{
|
||||||
'items-end': message.chatType === 1,
|
'items-end': message.chatType === 1,
|
||||||
'items-start': message.chatType === 2,
|
'items-start': message.chatType === 2,
|
||||||
|
'tool-message': message.chatType === 3,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- 消息气泡 -->
|
<!-- 消息气泡 -->
|
||||||
@@ -263,11 +356,39 @@ onUnload(() => {
|
|||||||
class="shadow-message break-words rounded-[20rpx] p-[24rpx] leading-[1.4]"
|
class="shadow-message break-words rounded-[20rpx] p-[24rpx] leading-[1.4]"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-[#336cff] text-white': message.chatType === 1,
|
'bg-[#336cff] text-white': message.chatType === 1,
|
||||||
'bg-white text-[#232338] border border-[#eeeeee]': message.chatType === 2,
|
'bg-white text-[#232338] border border-[#eeeeee]': [2, 3].includes(message.chatType),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<template v-if="Array.isArray(extractContentFromString(message.content))">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div v-for="(item, idx) in extractContentFromString(message.content)" :key="idx">
|
||||||
|
<div v-if="item.type === 'text'" class="text-content">
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.type === 'tool'" class="tool-call-text">
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.type === 'tool_result'" class="tool-call-text">
|
||||||
|
<div v-if="item.text && item.text.length > 80" class="tool-result-wrapper">
|
||||||
|
<div v-if="isToolResultCollapsed(index, idx)" class="tool-result-collapsed">
|
||||||
|
{{ getFirstLineText(item.text) }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="tool-result-expanded">
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
<span class="tool-toggle-btn" @click="toggleToolResult(index, idx)">
|
||||||
|
<wd-icon :name="isToolResultCollapsed(index, idx) ? 'arrow-down' : 'arrow-up'" size="12" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<!-- 内容区域 - 使用flex布局让图标和文本对齐 -->
|
<!-- 内容区域 - 使用flex布局让图标和文本对齐 -->
|
||||||
<view class="flex items-center gap-[12rpx]">
|
<view v-else class="flex items-center gap-[12rpx]">
|
||||||
<!-- 音频播放图标 -->
|
<!-- 音频播放图标 -->
|
||||||
<view
|
<view
|
||||||
v-if="message.audioId"
|
v-if="message.audioId"
|
||||||
@@ -334,4 +455,46 @@ onUnload(() => {
|
|||||||
.animate-pulse-audio {
|
.animate-pulse-audio {
|
||||||
animation: pulse-audio 1.5s infinite;
|
animation: pulse-audio 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-text {
|
||||||
|
color: #1890ff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24rpx;
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .tool-call-text {
|
||||||
|
color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-message .message-content {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-result-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-result-collapsed {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ import type { ChatSession } from '@/api/chat-history/types'
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { getChatSessions } from '@/api/chat-history/chat-history'
|
import { getChatSessions } from '@/api/chat-history/chat-history'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { deepClone } from '@/utils'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ChatHistory',
|
name: 'ChatHistory',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
agentId: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
// 接收props
|
// 接收props
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: string
|
agentId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
agentId: 'default'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取屏幕边界到安全区域距离
|
// 获取屏幕边界到安全区域距离
|
||||||
let safeAreaInsets: any
|
let safeAreaInsets: any
|
||||||
let systemInfo: any
|
let systemInfo: any
|
||||||
@@ -43,7 +44,7 @@ const sessionList = ref<ChatSession[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const hasMore = ref(true)
|
const hasMore = ref(true)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(0)
|
||||||
const pageSize = 10
|
const pageSize = 10
|
||||||
|
|
||||||
// 使用传入的智能体ID
|
// 使用传入的智能体ID
|
||||||
@@ -52,10 +53,8 @@ const currentAgentId = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载聊天会话列表
|
// 加载聊天会话列表
|
||||||
async function loadChatSessions(page = 1, isRefresh = false) {
|
async function loadChatSessions(page = 1, isUpdate = false) {
|
||||||
try {
|
try {
|
||||||
console.log(t('chatHistory.getChatSessions'), { page, isRefresh })
|
|
||||||
|
|
||||||
// 检查是否有当前选中的智能体
|
// 检查是否有当前选中的智能体
|
||||||
if (!currentAgentId.value) {
|
if (!currentAgentId.value) {
|
||||||
console.warn(t('chatHistory.noSelectedAgent'))
|
console.warn(t('chatHistory.noSelectedAgent'))
|
||||||
@@ -76,7 +75,10 @@ async function loadChatSessions(page = 1, isRefresh = false) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
sessionList.value = response.list || []
|
const oldSessionList = deepClone(sessionList.value)
|
||||||
|
oldSessionList.splice(0, 10)
|
||||||
|
oldSessionList.unshift(...(response.list || []))
|
||||||
|
sessionList.value = isUpdate ? oldSessionList : response.list || []
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sessionList.value.push(...(response.list || []))
|
sessionList.value.push(...(response.list || []))
|
||||||
@@ -170,10 +172,15 @@ function goToChatDetail(session: ChatSession) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 智能体已简化为默认
|
// 智能体已简化为默认
|
||||||
|
|
||||||
loadChatSessions(1)
|
loadChatSessions(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (currentPage.value !== 0) {
|
||||||
|
loadChatSessions(1, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refresh,
|
refresh,
|
||||||
@@ -187,8 +194,8 @@ defineExpose({
|
|||||||
<view v-if="loading && sessionList.length === 0" class="loading-container">
|
<view v-if="loading && sessionList.length === 0" class="loading-container">
|
||||||
<wd-loading color="#336cff" />
|
<wd-loading color="#336cff" />
|
||||||
<text class="loading-text">
|
<text class="loading-text">
|
||||||
{{ t('chatHistory.loading') }}
|
{{ t('chatHistory.loading') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
@@ -205,7 +212,7 @@ defineExpose({
|
|||||||
<view class="session-info">
|
<view class="session-info">
|
||||||
<view class="session-header">
|
<view class="session-header">
|
||||||
<text class="session-title">
|
<text class="session-title">
|
||||||
{{ t('chatHistory.conversationRecord') }} {{ session.sessionId.substring(0, 8) }}...
|
{{ session.title || `${t('chatHistory.conversationRecord')} ${session.sessionId.substring(0, 8)}...` }}
|
||||||
</text>
|
</text>
|
||||||
<text class="session-time">
|
<text class="session-time">
|
||||||
{{ formatTime(session.createdAt) }}
|
{{ formatTime(session.createdAt) }}
|
||||||
@@ -242,11 +249,11 @@ defineExpose({
|
|||||||
<view v-else-if="!loading" class="empty-state">
|
<view v-else-if="!loading" class="empty-state">
|
||||||
<wd-icon name="chat" custom-class="empty-icon" />
|
<wd-icon name="chat" custom-class="empty-icon" />
|
||||||
<text class="empty-text">
|
<text class="empty-text">
|
||||||
{{ t('chatHistory.noChatRecords') }}
|
{{ t('chatHistory.noChatRecords') }}
|
||||||
</text>
|
</text>
|
||||||
<text class="empty-desc">
|
<text class="empty-desc">
|
||||||
{{ t('chatHistory.chatRecordsDescription') }}
|
{{ t('chatHistory.chatRecordsDescription') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -333,7 +340,7 @@ defineExpose({
|
|||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
|
|
||||||
.session-info {
|
.session-info {
|
||||||
flex: 1;
|
width: 94%;
|
||||||
|
|
||||||
.session-header {
|
.session-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -345,8 +352,11 @@ defineExpose({
|
|||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #232338;
|
color: #232338;
|
||||||
max-width: 70%;
|
width: 70%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-time {
|
.session-time {
|
||||||
|
|||||||
@@ -300,3 +300,36 @@ export function debounce<T extends AnyFunction>(
|
|||||||
|
|
||||||
return debounced
|
return debounced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeepCloneTarget = string | number | boolean | null | undefined | object
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝方法
|
||||||
|
* @param target 要拷贝的目标
|
||||||
|
* @returns 拷贝后的新对象
|
||||||
|
*/
|
||||||
|
export function deepClone<T extends DeepCloneTarget>(target: T): T {
|
||||||
|
if (target === null || typeof target !== 'object') {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof Date) {
|
||||||
|
return new Date(target.getTime()) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(target)) {
|
||||||
|
return target.map(item => deepClone(item)) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof Object) {
|
||||||
|
const clonedObj = {} as T
|
||||||
|
for (const key in target) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
||||||
|
(clonedObj as any)[key] = deepClone((target as any)[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clonedObj
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user