feat: align agent preview with learner topics
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -562,6 +562,7 @@ export interface TopicSessionRecord {
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
summary?: TopicSummaryRecord | null;
|
||||
summaryAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface QuestionInsightSummary {
|
||||
|
||||
Reference in New Issue
Block a user