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:
2026-07-26 20:26:15 +08:00
parent bfce6ef932
commit cf2a9c7695

View File

@@ -151,8 +151,8 @@
<el-tag size="small" :type="agent.status === 'published' ? 'success' : 'warning'">{{ agent.status === 'published' ? '已发布' : agent.status }}</el-tag>
</div>
<p class="hint">可以问我任何问题,或者试试下面的示例:</p>
<div class="preset-questions">
<div class="preset-q" v-for="(q, qi) in getPresetQuestions()" :key="qi" @click="sendPresetQuestion(q)">{{ q }}</div>
<div class="preset-questions" v-loading="presetLoading" element-loading-text="正在生成示例问题...">
<div class="preset-q" v-for="(q, qi) in presetQuestions" :key="qi" @click="sendPresetQuestion(q)">{{ q }}</div>
</div>
</template>
<template v-else-if="chatMode === 'single'">
@@ -186,10 +186,6 @@
<el-tag size="small" type="info">{{ msg.orchestrateResult.mode }}</el-tag>
<span class="orch-agent-count">{{ msg.orchestrateResult.steps.length }} 个 Agent</span>
</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 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">
@@ -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 === '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 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>
</template>
</ChatMessageBubble>
@@ -452,7 +448,7 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
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 MainLayout from '@/components/MainLayout.vue'
import ChatMessageBubble from '@/components/ChatMessageBubble.vue'
@@ -851,12 +847,13 @@ async function loadAgents() {
}
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 {
const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`)
agent.value = resp.data
await loadSessions()
await loadBranches()
loadPresetQuestions()
saveState()
nextTick(scrollToBottom)
} catch { ElMessage.error('加载 Agent 失败'); agent.value = null }
@@ -894,16 +891,47 @@ function newSession() {
nextTick(scrollToBottom)
}
function getPresetQuestions(): string[] {
const name = agent.value?.name || ''; const desc = agent.value?.description || ''
const combined = `${name} ${desc}`.toLowerCase()
if (combined.includes('代码') || combined.includes('编程') || combined.includes('开发'))
return ['帮我写一个 Python 脚本来处理 CSV 文件', '解释一下这段代码的作用和潜在问题', '如何设计一个 RESTful API 接口?']
if (combined.includes('写作') || combined.includes('文案') || combined.includes('文章'))
return ['帮我写一篇关于 AI 发展的文章大纲', '润色这段文字,让它更加流畅易读', '给我几个吸引眼球的标题方案']
if (combined.includes('分析') || combined.includes('数据') || combined.includes('报告'))
return ['分析这组数据的趋势和异常点', '帮我总结这份报告的核心要点', '对比 A 和 B 两个方案的优劣']
return ['介绍一下你的能力和可以使用的工具', '帮我分析一个复杂问题', '搜索并整理最新的相关信息']
// ── 预设问题:LLM 按 Agent 描述生成,localStorage 缓存(Agent 更新后自动失效)──
const presetQuestions = ref<string[]>([])
const presetLoading = ref(false)
function fallbackPresetQuestions(): string[] {
const name = agent.value?.name || '这个 Agent'
return [
`作为${name},你能帮我解决什么问题?`,
'给我一个你能处理的具体例子',
'我们从哪里开始比较好?',
]
}
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()) }
@@ -1068,7 +1096,15 @@ async function sendMessage() {
}
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] = []
if (chatMode.value === 'single') { sessionId.value[key] = ''; currentSessionId.value = '' }
currentBranchId.value = ''