feat: align agent preview with learner topics

This commit is contained in:
2026-07-31 18:21:36 +08:00
parent ab2c945f0b
commit b6aff78aac
18 changed files with 656 additions and 44 deletions

View File

@@ -3,7 +3,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { api, streamDebugAgent } from "../services/api";
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem, TopicSessionRecord } from "../types/api";
import AgentGenerationParameters from "./AgentGenerationParameters.vue";
import AgentResponseDepthControl from "./AgentResponseDepthControl.vue";
import AdminPagination from "./AdminPagination.vue";
@@ -27,6 +27,8 @@ const models = ref<ModelItem[]>([]);
const knowledge = ref<KnowledgeItem[]>([]);
const debugUsers = ref<AdminUser[]>([]);
const debugUserLoading = ref(false);
const debugTopics = ref<TopicSessionRecord[]>([]);
const debugTopicLoading = ref(false);
const history = ref<PromptHistoryItem[]>([]);
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
const historyDetailOpen = ref(false);
@@ -47,6 +49,7 @@ const agentPreviewMessages = ref<{
const agentForm = reactive({
modelId: undefined as number | undefined,
userId: undefined as number | undefined,
topicSessionId: undefined as number | undefined,
knowledgeIds: [] as number[],
temperature: 0.2 as number | null,
topP: null as number | null,
@@ -89,6 +92,11 @@ const selectedDebugUserLabel = computed(() => {
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
});
const selectedDebugTopicLabel = computed(() => {
if (!agentForm.topicSessionId) return "未指定主题";
const topic = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
return topic ? topic.title : `主题 #${agentForm.topicSessionId}`;
});
onMounted(load);
@@ -135,6 +143,55 @@ async function searchDebugUsers(keyword: string) {
}
}
async function handleDebugUserChange(userId?: number) {
agentForm.topicSessionId = undefined;
debugTopics.value = [];
clearAgentPreview("模拟学员已切换,可以选择其中一个主题继续调试。");
if (!userId) return;
debugTopicLoading.value = true;
try {
await loadDebugTopics(userId);
} catch (error) {
ElMessage.error(errorMessage(error, "学员主题加载失败"));
} finally {
debugTopicLoading.value = false;
}
}
async function loadDebugTopics(userId: number, keyword = "") {
const selected = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
const result = await api.userTopics(userId, { keyword, page: 1, pageSize: 100 });
debugTopics.value = selected && !result.items.some((item) => item.id === selected.id)
? [selected, ...result.items]
: result.items;
}
async function searchDebugTopics(keyword: string) {
if (!agentForm.userId) return;
debugTopicLoading.value = true;
try {
await loadDebugTopics(agentForm.userId, keyword);
} catch (error) {
ElMessage.error(errorMessage(error, "学员主题搜索失败"));
} finally {
debugTopicLoading.value = false;
}
}
function handleDebugTopicChange() {
clearAgentPreview(
agentForm.topicSessionId
? "模拟主题已切换,下一条调试会加载该主题的摘要和最近对话。"
: "已恢复通用学员模拟,不加载具体主题对话。",
);
}
function debugTopicOptionLabel(topic: TopicSessionRecord) {
const statusLabel = topic.status === "active" ? "进行中" : topic.status === "completed" ? "已完成" : topic.status;
const summaryLabel = topic.summaryAvailable ? "有摘要" : "无摘要";
return `${topic.title} · ${statusLabel} · ${topic.messageCount} 条消息 · ${summaryLabel}`;
}
function applyRuntimeConfig(value: AgentRuntimeConfig) {
runtimeConfig.value = value;
Object.assign(runtimeForm, {
@@ -325,6 +382,7 @@ async function debugAgent() {
promptContent: promptContent.value,
modelId: agentForm.modelId,
userId: agentForm.userId || null,
topicSessionId: agentForm.topicSessionId || null,
knowledgeIds: agentForm.knowledgeIds,
question,
history: conversationHistory,
@@ -385,8 +443,8 @@ function scrollAgentPreview() {
});
}
function clearAgentPreview() {
agentPreviewMessages.value = [{ role: "assistant", content: "预览已清空,可以继续发起新的 Agent 调试。" }];
function clearAgentPreview(message = "预览已清空,可以继续发起新的 Agent 调试。") {
agentPreviewMessages.value = [{ role: "assistant", content: message }];
agentDebugTrace.value = [];
lastDebugRoute.value = null;
}
@@ -501,25 +559,50 @@ function errorMessage(error: unknown, fallback: string) {
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
</el-select>
</el-form-item>
<el-form-item label="模拟学员(可选)">
<el-select
v-model="agentForm.userId"
clearable
filterable
remote
:remote-method="searchDebugUsers"
:loading="debugUserLoading"
placeholder="不选择则不注入学员权益和成长档案"
>
<el-option
v-for="user in debugUsers"
:key="user.id"
:label="`${user.name || user.nickname || user.phone} · ${user.phone} · #${user.id}`"
:value="user.id"
/>
</el-select>
<div class="agent-form-help">用于让后台预览加载该学员的权益主题额度和长期成长档案不会消耗额度也不会影响正式会话</div>
</el-form-item>
<div class="agent-debug-context-grid">
<el-form-item label="模拟学员(可选)">
<el-select
v-model="agentForm.userId"
clearable
filterable
remote
:remote-method="searchDebugUsers"
:loading="debugUserLoading"
:disabled="agentDebugging"
placeholder="不选择则不注入学员权益和成长档案"
@change="handleDebugUserChange"
>
<el-option
v-for="user in debugUsers"
:key="user.id"
:label="`${user.name || user.nickname || user.phone} · ${user.phone} · #${user.id}`"
:value="user.id"
/>
</el-select>
<div class="agent-form-help">加载该学员的权益和长期成长档案不会消耗额度或改动正式会话</div>
</el-form-item>
<el-form-item label="模拟主题(可选)">
<el-select
v-model="agentForm.topicSessionId"
clearable
filterable
remote
:remote-method="searchDebugTopics"
:loading="debugTopicLoading"
:disabled="!agentForm.userId || agentDebugging"
placeholder="选择学员后可加载具体主题"
@change="handleDebugTopicChange"
>
<el-option
v-for="topic in debugTopics"
:key="topic.id"
:label="debugTopicOptionLabel(topic)"
:value="topic.id"
/>
</el-select>
<div class="agent-form-help">选择后会加载该主题摘要和最近对话用于验证追问效果</div>
</el-form-item>
</div>
<el-form-item label="调试知识库">
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
@@ -593,7 +676,7 @@ function errorMessage(error: unknown, fallback: string) {
<aside class="agent-preview-panel">
<div class="agent-preview-head">
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }}</small></div>
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }} · {{ selectedDebugTopicLabel }}</small></div>
<div class="agent-preview-head-actions"><span :title="lastDebugRoute?.routeReason || ''">{{ lastDebugRoute?.modelName || selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div>
</div>
<div ref="agentPreviewChat" class="agent-preview-chat">

View File

@@ -1,6 +1,7 @@
import {
ElAlert,
ElButton,
ElCheckbox,
ElCollapse,
ElCollapseItem,
ElDatePicker,
@@ -32,6 +33,7 @@ import {
import "element-plus/theme-chalk/base.css";
import "element-plus/theme-chalk/el-alert.css";
import "element-plus/theme-chalk/el-button.css";
import "element-plus/theme-chalk/el-checkbox.css";
import "element-plus/theme-chalk/el-collapse.css";
import "element-plus/theme-chalk/el-date-picker.css";
import "element-plus/theme-chalk/el-divider.css";
@@ -70,6 +72,7 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn };
[
ElAlert,
ElButton,
ElCheckbox,
ElCollapse,
ElCollapseItem,
ElDatePicker,

View File

@@ -34,6 +34,7 @@ import type {
PromptHistoryItem,
QuestionInsightRefreshResult,
QuestionInsightSummary,
TopicSessionRecord,
} from "../types/api";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
@@ -129,6 +130,8 @@ export const api = {
},
users: (query: { keyword?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
userDetail: (id: number) => request<AdminUserDetail>(`/admin/user/${id}/detail`),
userTopics: (id: number, query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
request<PageResult<TopicSessionRecord>>(`/admin/user/${id}/topics${queryString(query)}`),
userReports: (id: number, limit = 20) => request<PeriodicReportRecord[]>(`/admin/user/${id}/reports${queryString({ limit })}`),
generateUserReport: (id: number, payload: { reportType: string; periodStart?: string | null; periodEnd?: string | null }) =>
request<PeriodicReportRecord>(`/admin/user/${id}/reports/generate`, { method: "POST", body: JSON.stringify(payload) }),

View File

@@ -1379,6 +1379,16 @@ textarea {
line-height: 1.6;
}
.agent-debug-context-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.agent-debug-context-grid .el-form-item {
min-width: 0;
}
.debug-knowledge-select { width: 100%; min-width: 0; }
.debug-knowledge-select .el-select__wrapper { height: 40px; min-height: 40px; overflow: hidden; }
.debug-knowledge-select .el-select__selection { flex-wrap: nowrap; overflow: hidden; }
@@ -2385,6 +2395,10 @@ textarea {
.agent-page-head {
flex-direction: column;
}
.agent-debug-context-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 860px) {

View File

@@ -562,6 +562,7 @@ export interface TopicSessionRecord {
createdAt?: string | null;
updatedAt?: string | null;
summary?: TopicSummaryRecord | null;
summaryAvailable?: boolean;
}
export interface QuestionInsightSummary {