feat: AgentChat UX fixes — LLM preset questions, regenerate, clear confirm
- preset questions now generated by LLM from agent name+description and cached in localStorage (keyed by agent id+updated_at); fixes keyword heuristic mismatch (e.g. 购房专家 got data-analysis prompts) - add regenerate button on all assistant messages (was error-only retry) - clear chat now asks for confirmation, notes session history is kept - remove duplicate final-answer render in orchestrate result (bubble body already shows it) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,8 +151,8 @@
|
|||||||
<el-tag size="small" :type="agent.status === 'published' ? 'success' : 'warning'">{{ agent.status === 'published' ? '已发布' : agent.status }}</el-tag>
|
<el-tag size="small" :type="agent.status === 'published' ? 'success' : 'warning'">{{ agent.status === 'published' ? '已发布' : agent.status }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">可以问我任何问题,或者试试下面的示例:</p>
|
<p class="hint">可以问我任何问题,或者试试下面的示例:</p>
|
||||||
<div class="preset-questions">
|
<div class="preset-questions" v-loading="presetLoading" element-loading-text="正在生成示例问题...">
|
||||||
<div class="preset-q" v-for="(q, qi) in getPresetQuestions()" :key="qi" @click="sendPresetQuestion(q)">{{ q }}</div>
|
<div class="preset-q" v-for="(q, qi) in presetQuestions" :key="qi" @click="sendPresetQuestion(q)">{{ q }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="chatMode === 'single'">
|
<template v-else-if="chatMode === 'single'">
|
||||||
@@ -186,10 +186,6 @@
|
|||||||
<el-tag size="small" type="info">{{ msg.orchestrateResult.mode }}</el-tag>
|
<el-tag size="small" type="info">{{ msg.orchestrateResult.mode }}</el-tag>
|
||||||
<span class="orch-agent-count">{{ msg.orchestrateResult.steps.length }} 个 Agent</span>
|
<span class="orch-agent-count">{{ msg.orchestrateResult.steps.length }} 个 Agent</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="orch-final">
|
|
||||||
<div class="orch-section-title">最终回答</div>
|
|
||||||
<div class="message-text" v-html="renderMarkdown(msg.orchestrateResult.final_answer)"></div>
|
|
||||||
</div>
|
|
||||||
<div class="orch-steps">
|
<div class="orch-steps">
|
||||||
<div v-for="(step, si) in msg.orchestrateResult.steps" :key="si" class="orch-step" :class="{ expanded: step._open }">
|
<div v-for="(step, si) in msg.orchestrateResult.steps" :key="si" class="orch-step" :class="{ expanded: step._open }">
|
||||||
<div class="orch-step-header" @click="step._open = !step._open">
|
<div class="orch-step-header" @click="step._open = !step._open">
|
||||||
@@ -257,7 +253,7 @@
|
|||||||
<el-button v-if="msg.role === 'user'" link size="small" @click="editMessage(i)" title="编辑"><el-icon><Edit /></el-icon></el-button>
|
<el-button v-if="msg.role === 'user'" link size="small" @click="editMessage(i)" title="编辑"><el-icon><Edit /></el-icon></el-button>
|
||||||
<el-button v-if="msg.role === 'assistant'" link size="small" @click="copyMessage(msg)" title="复制"><el-icon><DocumentCopy /></el-icon></el-button>
|
<el-button v-if="msg.role === 'assistant'" link size="small" @click="copyMessage(msg)" title="复制"><el-icon><DocumentCopy /></el-icon></el-button>
|
||||||
<el-button link size="small" @click="createBranch(i)" title="从此分支"><el-icon><Share /></el-icon></el-button>
|
<el-button link size="small" @click="createBranch(i)" title="从此分支"><el-icon><Share /></el-icon></el-button>
|
||||||
<el-button v-if="msg.status === 'error'" link type="danger" size="small" @click="retryMessage(i)" title="重试"><el-icon><Refresh /></el-icon></el-button>
|
<el-button v-if="msg.role === 'assistant' && !loading" link :type="msg.status === 'error' ? 'danger' : undefined" size="small" @click="retryMessage(i)" :title="msg.status === 'error' ? '重试' : '重新生成'"><el-icon><Refresh /></el-icon></el-button>
|
||||||
<el-button link size="small" @click="deleteMessage(i)" title="删除"><el-icon style="color:var(--el-color-danger)"><Delete /></el-icon></el-button>
|
<el-button link size="small" @click="deleteMessage(i)" title="删除"><el-icon style="color:var(--el-color-danger)"><Delete /></el-icon></el-button>
|
||||||
</template>
|
</template>
|
||||||
</ChatMessageBubble>
|
</ChatMessageBubble>
|
||||||
@@ -452,7 +448,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ChatLineSquare, UserFilled, Promotion, Tools, CaretRight, ChatDotSquare, Select, DocumentCopy, Refresh, VideoPause, Edit, Delete, Headset, Setting, Search, Download, Share, MagicStick } from '@element-plus/icons-vue'
|
import { ChatLineSquare, UserFilled, Promotion, Tools, CaretRight, ChatDotSquare, Select, DocumentCopy, Refresh, VideoPause, Edit, Delete, Headset, Setting, Search, Download, Share, MagicStick } from '@element-plus/icons-vue'
|
||||||
import MainLayout from '@/components/MainLayout.vue'
|
import MainLayout from '@/components/MainLayout.vue'
|
||||||
import ChatMessageBubble from '@/components/ChatMessageBubble.vue'
|
import ChatMessageBubble from '@/components/ChatMessageBubble.vue'
|
||||||
@@ -851,12 +847,13 @@ async function loadAgents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchAgent() {
|
async function switchAgent() {
|
||||||
if (!currentAgentId.value) { agent.value = null; currentSessionId.value = ''; sessions.value = []; saveState(); nextTick(scrollToBottom); return }
|
if (!currentAgentId.value) { agent.value = null; currentSessionId.value = ''; sessions.value = []; presetQuestions.value = []; saveState(); nextTick(scrollToBottom); return }
|
||||||
try {
|
try {
|
||||||
const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`)
|
const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`)
|
||||||
agent.value = resp.data
|
agent.value = resp.data
|
||||||
await loadSessions()
|
await loadSessions()
|
||||||
await loadBranches()
|
await loadBranches()
|
||||||
|
loadPresetQuestions()
|
||||||
saveState()
|
saveState()
|
||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
} catch { ElMessage.error('加载 Agent 失败'); agent.value = null }
|
} catch { ElMessage.error('加载 Agent 失败'); agent.value = null }
|
||||||
@@ -894,16 +891,47 @@ function newSession() {
|
|||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPresetQuestions(): string[] {
|
// ── 预设问题:LLM 按 Agent 描述生成,localStorage 缓存(Agent 更新后自动失效)──
|
||||||
const name = agent.value?.name || ''; const desc = agent.value?.description || ''
|
const presetQuestions = ref<string[]>([])
|
||||||
const combined = `${name} ${desc}`.toLowerCase()
|
const presetLoading = ref(false)
|
||||||
if (combined.includes('代码') || combined.includes('编程') || combined.includes('开发'))
|
|
||||||
return ['帮我写一个 Python 脚本来处理 CSV 文件', '解释一下这段代码的作用和潜在问题', '如何设计一个 RESTful API 接口?']
|
function fallbackPresetQuestions(): string[] {
|
||||||
if (combined.includes('写作') || combined.includes('文案') || combined.includes('文章'))
|
const name = agent.value?.name || '这个 Agent'
|
||||||
return ['帮我写一篇关于 AI 发展的文章大纲', '润色这段文字,让它更加流畅易读', '给我几个吸引眼球的标题方案']
|
return [
|
||||||
if (combined.includes('分析') || combined.includes('数据') || combined.includes('报告'))
|
`作为${name},你能帮我解决什么问题?`,
|
||||||
return ['分析这组数据的趋势和异常点', '帮我总结这份报告的核心要点', '对比 A 和 B 两个方案的优劣']
|
'给我一个你能处理的具体例子',
|
||||||
return ['介绍一下你的能力和可以使用的工具', '帮我分析一个复杂问题', '搜索并整理最新的相关信息']
|
'我们从哪里开始比较好?',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPresetQuestions() {
|
||||||
|
const a = agent.value
|
||||||
|
if (!a) { presetQuestions.value = []; return }
|
||||||
|
const cacheKey = `agent_preset_qs:${a.id}:${a.updated_at || ''}`
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(cacheKey)
|
||||||
|
if (cached) {
|
||||||
|
const qs = JSON.parse(cached)
|
||||||
|
if (Array.isArray(qs) && qs.length) { presetQuestions.value = qs; return }
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
presetLoading.value = true
|
||||||
|
try {
|
||||||
|
const body: any = {
|
||||||
|
message: `你是「${a.name}」。简介:${a.description || '无'}。\n请生成 3 个用户最可能向你提问的示例问题(每个不超过 30 字)。只返回 JSON 数组,例如 ["问题1","问题2","问题3"],不要输出任何其他内容。`,
|
||||||
|
model: 'deepseek-v4-flash', max_iterations: 1, temperature: 0.7,
|
||||||
|
}
|
||||||
|
if (chatModelConfigId.value) body.model_config_id = chatModelConfigId.value
|
||||||
|
const resp = await api.post('/api/v1/agent-chat/bare', body)
|
||||||
|
const content = resp.data?.content || ''
|
||||||
|
const m = content.match(/\[[\s\S]*\]/)
|
||||||
|
const qs = m ? JSON.parse(m[0]) : []
|
||||||
|
const valid = Array.isArray(qs) ? qs.filter(q => typeof q === 'string' && q.trim()).map(q => q.trim()).slice(0, 3) : []
|
||||||
|
presetQuestions.value = valid.length ? valid : fallbackPresetQuestions()
|
||||||
|
if (valid.length) localStorage.setItem(cacheKey, JSON.stringify(valid))
|
||||||
|
} catch {
|
||||||
|
presetQuestions.value = fallbackPresetQuestions()
|
||||||
|
} finally { presetLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPresetQuestion(q: string) { inputMessage.value = q; nextTick(() => sendMessage()) }
|
function sendPresetQuestion(q: string) { inputMessage.value = q; nextTick(() => sendMessage()) }
|
||||||
@@ -1068,7 +1096,15 @@ async function sendMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleTrace(msg: ChatMessage) { msg._traceOpen = !msg._traceOpen }
|
function toggleTrace(msg: ChatMessage) { msg._traceOpen = !msg._traceOpen }
|
||||||
function clearChat() {
|
async function clearChat() {
|
||||||
|
if (displayMessages.value.length === 0) return
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'清空后将开始新对话(历史会话仍保留在「对话记录」中)。',
|
||||||
|
'确认清空',
|
||||||
|
{ type: 'warning', confirmButtonText: '清空', cancelButtonText: '取消' }
|
||||||
|
)
|
||||||
|
} catch { return }
|
||||||
const key = currentAgentKey.value; messages.value[key] = []
|
const key = currentAgentKey.value; messages.value[key] = []
|
||||||
if (chatMode.value === 'single') { sessionId.value[key] = ''; currentSessionId.value = '' }
|
if (chatMode.value === 'single') { sessionId.value[key] = ''; currentSessionId.value = '' }
|
||||||
currentBranchId.value = ''
|
currentBranchId.value = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user