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>
|
||||
</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 = ''
|
||||
|
||||
Reference in New Issue
Block a user