维护 Agent 对话

This commit is contained in:
Wenjie Zhang 2025-03-30 21:40:49 +08:00
parent cde6b98336
commit 58df6a599e
4 changed files with 74 additions and 105 deletions

View File

@ -79,20 +79,11 @@
v-for="message in conv.messages"
:message="message"
:key="message.id"
:role="message.role"
:content="message.text"
:content-html="message.text.length > 0 ? renderMarkdown(message) : ''"
:reasoning-content="message.reasoning_content"
:reasoning-header="message.status=='reasoning' ? '正在思考...' : '推理过程'"
:status="message.status"
:is-processing="isStreaming"
:error-message="message.message"
:show-refs="true"
@retry="retryMessage(message.id)"
@retryStoppedMessage="retryStoppedMessage(message.id)"
>
<template #refs v-if="message.role=='received' && message.status=='finished'">
<RefsComponent :message="message" :conv="conv" @retry="retryMessage(message.id)" />
</template>
</MessageComponent>
</div>
<div class="bottom">
@ -178,13 +169,8 @@ import {
CopyOutlined
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import { useConfigStore } from '@/stores/config'
import { message } from 'ant-design-vue'
import RefsComponent from '@/components/RefsComponent.vue'
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import MessageInputComponent from '@/components/MessageInputComponent.vue'
import MessageComponent from '@/components/MessageComponent.vue'
@ -232,39 +218,18 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
wideScreen: false,
})
const marked = new Marked(
{
gfm: true,
breaks: true,
tables: true,
},
markedHighlight({
langPrefix: 'hljs language-',
highlight(code) {
return hljs.highlightAuto(code).value;
}
})
);
const consoleMsg = (msg) => console.log(msg)
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))
onClickOutside(modelCard, () => setTimeout(() => opts.showModelCard = false, 30))
const renderMarkdown = (msg) => {
if (msg.status === 'loading') {
return marked.parse(msg.text + '🟢')
} else {
return marked.parse(msg.text)
}
}
// message history {role, content}
const getHistory = () => {
const history = conv.value.messages.map((msg) => {
if (msg.text) {
if (msg.content) {
return {
role: msg.role === 'sent' ? 'user' : 'assistant',
content: msg.text
content: msg.content
}
}
}).reduce((acc, cur) => {
@ -309,15 +274,15 @@ const handleKeyDown = (e) => {
const renameTitle = () => {
if (meta.summary_title) {
const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:'
const firstUserMessage = conv.value.messages[0].text
const firstAiMessage = conv.value.messages[1].text
const firstUserMessage = conv.value.messages[0].content
const firstAiMessage = conv.value.messages[1].content
const context = `${prompt}\n\n问题: ${firstUserMessage}\n\n回复: ${firstAiMessage},主题是(一句话):`
simpleCall(context).then((data) => {
const response = data.response.split("")[0].replace(/^["'"']/g, '').replace(/["'"']$/g, '')
emit('rename-title', response)
})
} else {
emit('rename-title', conv.value.messages[0].text)
emit('rename-title', conv.value.messages[0].content)
}
}
@ -353,16 +318,16 @@ const appendUserMessage = (msg) => {
conv.value.messages.push({
id: generateRandomHash(16),
role: 'sent',
text: msg
content: msg
})
scrollToBottom()
}
const appendAiMessage = (text, refs=null) => {
const appendAiMessage = (content, refs=null) => {
conv.value.messages.push({
id: generateRandomHash(16),
role: 'received',
text: text,
content: content,
reasoning_content: '',
refs,
status: "init",
@ -377,8 +342,8 @@ const updateMessage = (info) => {
if (msg) {
try {
// text
if (info.text !== null && info.text !== undefined && info.text !== '') {
msg.text += info.text;
if (info.content !== null && info.content !== undefined && info.content !== '') {
msg.content += info.content;
}
if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') {
@ -415,7 +380,7 @@ const updateMessage = (info) => {
} catch (error) {
console.error('Error updating message:', error);
msg.status = 'error';
msg.text = '消息更新失败';
msg.content = '消息更新失败';
}
} else {
console.error('Message not found:', info.id);
@ -512,13 +477,13 @@ const fetchChatResponse = (user_input, cur_res_id) => {
const data = JSON.parse(line);
updateMessage({
id: cur_res_id,
text: data.response,
content: data.response,
reasoning_content: data.reasoning_content,
status: data.status,
meta: data.meta,
...data,
});
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].text)
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].content)
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].status)
if (data.history) {
@ -588,7 +553,7 @@ const retryMessage = (id) => {
const index = conv.value.messages.findIndex(msg => msg.id === id);
const pastMessage = conv.value.messages[index-1]
console.log("retryMessage", id, pastMessage)
conv.value.inputText = pastMessage.text
conv.value.inputText = pastMessage.content
if (index !== -1) {
conv.value.messages = conv.value.messages.slice(0, index-1);
}
@ -606,7 +571,7 @@ onMounted(() => {
//
if (conv.value.messages && conv.value.messages.length > 0) {
conv.value.messages.forEach(msg => {
if (msg.role === 'received' && (!msg.text || msg.text.trim() === '')) {
if (msg.role === 'received' && (!msg.content || msg.content.trim() === '')) {
msg.status = 'error';
msg.message = '内容加载失败';
}
@ -669,7 +634,7 @@ const retryStoppedMessage = (id) => {
if (messageIndex > 0) {
const userMessage = conv.value.messages[messageIndex - 1];
if (userMessage && userMessage.role === 'sent') {
conv.value.inputText = userMessage.text;
conv.value.inputText = userMessage.content;
//
conv.value.messages = conv.value.messages.slice(0, messageIndex-1);
// sendMessage();

View File

@ -1,18 +1,18 @@
<template>
<div class="message-box" :class="[role, customClasses]">
<div class="message-box" :class="[message.role, customClasses]">
<!-- 用户消息 -->
<p v-if="role === 'user' || role === 'sent'" class="message-text">{{ content }}</p>
<p v-if="message.role === 'user' || message.role === 'sent'" class="message-text">{{ message.content }}</p>
<!-- 助手消息 -->
<div v-else-if="role === 'assistant' || role === 'received'" class="assistant-message">
<div v-else-if="message.role === 'assistant' || message.role === 'received'" class="assistant-message">
<!-- 推理过程 (ChatComponent特有) -->
<div v-if="reasoningContent" class="reasoning-box">
<div v-if="message.reasoning_content" class="reasoning-box">
<a-collapse v-model:activeKey="reasoningActiveKey" :bordered="false">
<template #expandIcon="{ isActive }">
<caret-right-outlined :rotate="isActive ? 90 : 0" />
</template>
<a-collapse-panel key="show" :header="reasoningHeader" class="reasoning-header">
<p class="reasoning-content">{{ reasoningContent }}</p>
<a-collapse-panel key="show" :header="message.status=='reasoning' ? '正在思考...' : '推理过程'" class="reasoning-header">
<p class="reasoning-content">{{ message.reasoning_content }}</p>
</a-collapse-panel>
</a-collapse>
</div>
@ -25,21 +25,21 @@
</div>
<!-- 检索中状态 (ChatComponent特有) -->
<div v-else-if="status === 'searching' && isProcessing" class="searching-msg">
<div v-else-if="message.status === 'searching' && isProcessing" class="searching-msg">
<i>正在检索</i>
</div>
<!-- 生成中状态 (ChatComponent特有) -->
<div v-else-if="status === 'generating' && isProcessing" class="searching-msg">
<div v-else-if="message.status === 'generating' && isProcessing" class="searching-msg">
<i>正在生成</i>
</div>
<div v-else-if="status === 'error'" class="err-msg" @click="$emit('retry')">
请求错误请重试{{ errorMessage }}
<div v-else-if="message.status === 'error'" class="err-msg" @click="$emit('retry')">
请求错误请重试{{ message.message }}
</div>
<!-- 消息内容 -->
<div v-else-if="contentHtml" v-html="contentHtml" class="message-md"></div>
<div v-else-if="message.content" v-html="renderMarkdown(message)" class="message-md"></div>
<div v-if="message.isStoppedByUser" class="retry-hint">
你停止生成了本次回答
@ -49,8 +49,9 @@
<!-- 工具调用 (AgentView特有) -->
<slot name="tool-calls"></slot>
<!-- 引用组件 (ChatComponent特有) -->
<slot name="refs"></slot>
<div v-if="(message.role=='received' || message.role=='assistant') && message.status=='finished' && showRefs">
<RefsComponent :message="message" @retry="emit('retry')" />
</div>
<!-- 错误消息 -->
</div>
@ -62,6 +63,13 @@
<script setup>
import { computed, ref } from 'vue';
import { CaretRightOutlined } from '@ant-design/icons-vue';
import RefsComponent from '@/components/RefsComponent.vue'
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
const props = defineProps({
// 'user'|'assistant'|'sent'|'received'
@ -69,49 +77,25 @@ const props = defineProps({
type: Object,
required: true
},
role: {
type: String,
required: true
},
//
content: {
type: String,
default: ''
},
// HTML
contentHtml: {
type: String,
default: ''
},
// (ChatComponent使)
reasoningContent: {
type: String,
default: ''
},
//
status: {
type: String,
default: ''
},
//
isProcessing: {
type: Boolean,
default: false
},
//
errorMessage: {
type: String,
default: ''
},
//
customClasses: {
type: Object,
default: () => ({})
},
//
reasoningHeader: {
type: String,
default: '推理过程'
//
showRefs: {
type: Boolean,
default: false
}
});
@ -128,10 +112,36 @@ const emit = defineEmits(['retry', 'retryStoppedMessage']);
//
const reasoningActiveKey = ref(['show']);
const renderMarkdown = (msg) => {
if (!msg.content) return '';
if (msg.status === 'loading') {
return marked.parse(msg.content + '🟢')
} else {
return marked.parse(msg.content)
}
}
const marked = new Marked(
{
gfm: true,
breaks: true,
tables: true,
},
markedHighlight({
langPrefix: 'hljs language-',
highlight(code) {
return hljs.highlightAuto(code).value;
}
})
);
//
const isEmptyAndLoading = computed(() => {
const isEmpty = !props.content || props.content.length === 0;
const isLoading = props.status === 'init'
const isEmpty = !props.message.content || props.message.content.length === 0;
const isLoading = props.message.status === 'init'
return isEmpty && isLoading;
});
</script>

View File

@ -3,8 +3,8 @@
<div class="tags">
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
<span class="item"><BulbOutlined /> {{ msg.meta.server_model_name }}</span>
<span class="item btn" @click="copyText(msg.text)" title="复制"><CopyOutlined /></span>
<span v-if="msg.meta?.server_model_name" class="item"><BulbOutlined /> {{ msg.meta.server_model_name }}</span>
<span class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined /></span>
<span class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined /></span>
<span
class="item btn"
@ -120,11 +120,9 @@ import GraphContainer from './GraphContainer.vue' // 导入 GraphContainer 组
const emit = defineEmits(['retry']);
const props = defineProps({
message: Object,
conv: Object,
})
const msg = ref(props.message)
const conv = ref(props.conv)
// 使 useClipboard
const { copy, isSupported } = useClipboard()
@ -166,7 +164,7 @@ const toggleDrawer = (filename) => {
openDetail[filename] = !openDetail[filename]
}
const showRefs = computed(() => msg.value.role=='received' && msg.value.status=='finished')
const showRefs = computed(() => (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished')
const subGraphVisible = ref(false)
const subGraphData = ref(null)

View File

@ -38,13 +38,9 @@
<div class="chat-box" ref="messagesContainer" :class="{ 'is-debug': options.debug_mode }">
<MessageComponent
v-for="(message, index) in messages"
:message="message"
:key="index"
:role="message.role"
:content="message.content"
:content-html="message.content ? renderMarkdown(message.content) : ''"
:status="message.status"
:is-processing="isProcessing"
:error-message="message.message"
@retry="retryMessage(index)"
>
<div v-if="options.debug_mode" class="status-info">{{ message }}</div>