feat(aihr): add prompt template management UI

This commit is contained in:
2026-07-08 08:42:06 +08:00
parent 4b10b08eeb
commit 2bdf527e26
3 changed files with 190 additions and 2 deletions
+1 -1
View File
@@ -127,7 +127,7 @@
- `frontend/src/api/aihr/practice.ts`:补场景/Rubric API。
- `frontend/src/views/train/practice.vue`:先加轻量场景选择和 Rubric 预览,不做复杂 CRUD。
- 提示词模板管理可先挂在“系统设置-模型配置”下:支持名称、代码、分类、输出格式、系统提示词、提示词模板、描述、启用状态;模板变量用 `{{变量名}}`。
- 提示词模板管理已挂在“系统设置-模型配置”下:支持名称、代码、分类、输出格式、系统提示词、提示词模板、描述、启用状态;模板变量用 `{{变量名}}`。
移动端:
+182 -1
View File
@@ -166,6 +166,38 @@
</el-table>
</section>
<section class="panel prompt-panel" v-loading="promptLoading">
<div class="panel-title with-actions">
<h2>提示词模板</h2>
<div>
<el-button :icon="Refresh" @click="loadPromptTemplates">刷新</el-button>
<el-button type="danger" :icon="Plus" @click="openPromptDialog()">新增模板</el-button>
</div>
</div>
<el-table :data="promptTemplates" class="model-table" size="large">
<el-table-column prop="code" label="代码" min-width="150" />
<el-table-column prop="name" label="名称" min-width="160" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="outputFormat" label="文体" width="120" />
<el-table-column prop="version" label="版本" width="100" />
<el-table-column label="启用" width="100">
<template #default="{ row }">
<el-switch
v-model="row.enabled"
active-color="#e60012"
inactive-color="#d7dee8"
@change="(value) => togglePromptEnabled(row, Boolean(value))"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button link type="danger" @click="openPromptDialog(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</section>
<div class="tip-bar">
<el-icon><InfoFilled /></el-icon>
<span>调用优先级:模型配置覆盖供应商默认值;未配置时回退 seed,接口不返回 API Key。</span>
@@ -241,6 +273,45 @@
<el-button type="danger" :loading="saving" @click="submitModel">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="promptDialogVisible" :title="promptForm.id > 0 ? '编辑提示词模板' : '新增提示词模板'" width="720px">
<el-form label-width="104px">
<el-form-item label="模板代码" required>
<el-input v-model="promptForm.code" placeholder="summary_card" />
</el-form-item>
<el-form-item label="模板名称" required>
<el-input v-model="promptForm.name" placeholder="总结卡" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="promptForm.category" placeholder="practice" />
</el-form-item>
<el-form-item label="输出文体">
<el-input v-model="promptForm.outputFormat" placeholder="summary_card" />
</el-form-item>
<el-form-item label="系统提示词">
<el-input v-model="promptForm.systemPrompt" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="提示词模板" required>
<el-input v-model="promptForm.template" type="textarea" :rows="5" />
</el-form-item>
<el-form-item label="变量">
<el-input v-model="promptForm.variables" placeholder='["question","answer"]' />
</el-form-item>
<el-form-item label="版本">
<el-input v-model="promptForm.version" placeholder="v1" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="promptForm.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="promptForm.enabled" active-text="启用" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="promptDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="promptSaving" @click="submitPromptTemplate">保存</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -266,6 +337,13 @@ import {
type ModelProviderPayload,
type VectorIndexStatus
} from '@/api/aihr/model';
import {
createPromptTemplate,
listPromptTemplates,
updatePromptTemplate,
type PromptTemplateRequest,
type PromptTemplateResponse
} from '@/api/aihr/practice';
type ProviderView = ModelProvider & { logo: string };
type ProviderFormState = ModelProviderPayload & {
@@ -282,6 +360,9 @@ type ModelFormState = ModelConfigPayload & {
apiKey: string;
modelDescribe: string;
};
type PromptTemplateFormState = PromptTemplateRequest & {
id: number;
};
const seedProviders: ProviderView[] = [
{
@@ -430,13 +511,17 @@ const keyword = ref('');
const loading = ref(false);
const testing = ref(false);
const saving = ref(false);
const promptLoading = ref(false);
const promptSaving = ref(false);
const vectorLoading = ref(false);
const rebuilding = ref(false);
const showKey = ref(false);
const providerDialogVisible = ref(false);
const modelDialogVisible = ref(false);
const promptDialogVisible = ref(false);
const lastTestAt = ref('未测试');
const vectorStatus = ref<VectorIndexStatus>();
const promptTemplates = ref<PromptTemplateResponse[]>([]);
const providerForm = reactive<ProviderFormState>({
id: 0,
@@ -463,6 +548,20 @@ const modelForm = reactive<ModelFormState>({
enabled: true
});
const promptForm = reactive<PromptTemplateFormState>({
id: 0,
code: '',
name: '',
category: 'practice',
outputFormat: '',
systemPrompt: '',
template: '',
variables: '',
version: 'v1',
enabled: true,
description: ''
});
const filteredProviders = computed(() => {
const word = keyword.value.trim().toLowerCase();
if (!word) return providers.value;
@@ -549,6 +648,18 @@ const loadVectorStatus = async () => {
}
};
const loadPromptTemplates = async () => {
promptLoading.value = true;
try {
const result = await listPromptTemplates();
promptTemplates.value = result.data ?? [];
} catch {
promptTemplates.value = [];
} finally {
promptLoading.value = false;
}
};
const runVectorRebuild = async () => {
try {
await ElMessageBox.confirm('将清空旧片段向量并重建 Qdrant 索引,继续吗?', '重建向量索引', {
@@ -734,9 +845,78 @@ const toggleModelEnabled = async (row: ModelConfig, enabled: boolean) => {
}
};
const openPromptDialog = (template?: PromptTemplateResponse) => {
Object.assign(promptForm, {
id: template?.id ?? 0,
code: template?.code ?? '',
name: template?.name ?? '',
category: template?.category ?? 'practice',
outputFormat: template?.outputFormat ?? '',
systemPrompt: template?.systemPrompt ?? '',
template: template?.template ?? '',
variables: template?.variables ?? '',
version: template?.version ?? 'v1',
enabled: template?.enabled ?? true,
description: template?.description ?? ''
});
promptDialogVisible.value = true;
};
const promptPayload = (): PromptTemplateRequest | undefined => {
const code = promptForm.code.trim();
const name = promptForm.name.trim();
const template = promptForm.template.trim();
if (!code || !name || !template) {
ElMessage.warning('请填写模板代码、名称和提示词模板');
return undefined;
}
return {
code,
name,
category: promptForm.category.trim() || 'practice',
outputFormat: promptForm.outputFormat.trim() || code,
systemPrompt: promptForm.systemPrompt.trim(),
template,
variables: promptForm.variables.trim(),
version: promptForm.version.trim() || 'v1',
enabled: promptForm.enabled,
description: promptForm.description.trim()
};
};
const submitPromptTemplate = async () => {
const payload = promptPayload();
if (!payload) return;
promptSaving.value = true;
try {
if (promptForm.id > 0) {
await updatePromptTemplate(promptForm.id, payload);
} else {
await createPromptTemplate(payload);
}
promptDialogVisible.value = false;
await loadPromptTemplates();
ElMessage.success('提示词模板已保存');
} finally {
promptSaving.value = false;
}
};
const togglePromptEnabled = async (row: PromptTemplateResponse, enabled: boolean) => {
try {
await updatePromptTemplate(row.id, { ...row, enabled });
row.enabled = enabled;
ElMessage.success(enabled ? '模板已启用' : '模板已停用');
} catch {
row.enabled = !enabled;
}
};
onMounted(() => {
loadData();
loadVectorStatus();
loadPromptTemplates();
});
</script>
@@ -941,7 +1121,8 @@ onMounted(() => {
.detail-panel,
.vector-panel,
.model-panel {
.model-panel,
.prompt-panel {
padding-bottom: 18px;
}
+7
View File
@@ -110,10 +110,13 @@ contains frontend/src/views/train/practice.vue "导师改写"
contains frontend/src/views/knowledge/cases.vue "真实语音"
contains frontend/src/views/knowledge/sop.vue "训练题已生成"
contains frontend/src/views/system/model/index.vue "调用优先级"
contains frontend/src/views/system/model/index.vue "提示词模板"
contains frontend/src/api/aihr/interview.ts "/api/recruit/interview/start"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "/api/recruit/interview"
contains frontend/src/api/aihr/practice.ts "/api/train/practice/start"
contains frontend/src/api/aihr/practice.ts "/api/aihr/prompt-templates"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "/api/train/practice"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java "/api/aihr/prompt-templates"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "mobileCompletedCount"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "aihr_practice_session"
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_scenario"
@@ -142,6 +145,9 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
contains mobile/src/App.vue "贡献度加分"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/reviews/{id}/reviewed"
contains mobile/src/App.vue "标记已复盘"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "source = 'retry'"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "maskSensitiveText"
contains mobile/src/App.vue "submitDailyDrill"
contains frontend/src/api/aihr/case.ts "/api/knowledge/case/upload"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "/api/knowledge/case"
contains frontend/src/api/aihr/sop.ts "/api/knowledge/search"
@@ -152,6 +158,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "/doc/upload"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "/embeddings"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap"
contains backend/script/sql/aihr_knowledge_mysql8.sql "embedding_json"
contains frontend/src/views/knowledge/sop.vue "上传文档"
contains frontend/src/views/knowledge/sop.vue ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"