第一次提交

This commit is contained in:
2026-04-03 09:56:14 +08:00
commit 60e2c8debd
3598 changed files with 746659 additions and 0 deletions

View File

@@ -0,0 +1,894 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:canFullscreen="true"
defaultFullscreen
destroyOnClose
title="评测调试"
:showOkBtn="false"
:showCancelBtn="false"
wrapClassName="evaluator-debug-modal"
>
<!-- 步骤条 -->
<a-steps :current="currentStep" class="steps-container">
<a-step title="选择提示词">
<template #icon>
<Icon icon="ant-design:file-search-outlined" />
</template>
</a-step>
<a-step title="调试配置">
<template #icon>
<Icon icon="ant-design:setting-outlined" />
</template>
</a-step>
</a-steps>
<!-- 第一步选择提示词和字段映射 -->
<div v-show="currentStep === 0" class="step-content step-one">
<div class="form-section">
<div class="form-item">
<label class="form-label">选择提示词</label>
<a-select v-model:value="selectedPromptKey" placeholder="请选择提示词" class="prompt-select" @change="onPromptChange">
<a-select-option v-for="prompt in promptOptions" :key="prompt.key" :value="prompt.key">
<span class="prompt-option">
<Icon icon="ant-design:file-text-outlined" />
{{ prompt.name }}
</span>
</a-select-option>
</a-select>
</div>
<div class="mapping-section" v-if="selectedPromptKey">
<div class="section-header">
<h3>
<Icon icon="ant-design:link-outlined" />
字段映射关系
</h3>
</div>
<div class="mapping-list">
<div class="mapping-item">
<div class="field-label">
<Icon icon="ant-design:tag-outlined" />
用户输入[user_query]
</div>
<div class="equal-sign">=</div>
<a-select allow-clear v-model:value="fieldMappings['user_query']" placeholder="请选择数据集字段" class="field-select">
<a-select-option v-for="column in datasetColumns" :key="column.key" :value="column.key">
<span class="column-option">
<span style="color:blue">评测集</span>
<span> [{{ column.title }}]</span>
<span style="margin-left: 5px">
<a-tooltip :title="column.description"><Icon icon="ant-design:info-circle-outlined" size="14" /></a-tooltip>
</span>
</span>
</a-select-option>
</a-select>
</div>
<div class="mapping-item" v-for="(item, index) in promptParams" :key="index">
<div class="field-label">
<Icon icon="ant-design:tag-outlined" />
变量[{{ item.name }}]
</div>
<div class="equal-sign">=</div>
<a-select allow-clear v-model:value="fieldMappings[item.name]" placeholder="请选择数据集字段" class="field-select">
<a-select-option v-for="column in datasetColumns" :key="column.key" :value="column.key">
<span class="column-option">
<span style="color:blue">评测集</span>
<span> [{{ column.title }}]</span>
<span style="margin-left: 5px">
<a-tooltip :title="column.description"><Icon icon="ant-design:info-circle-outlined" size="14" /></a-tooltip>
</span>
</span>
</a-select-option>
</a-select>
</div>
</div>
</div>
</div>
<div class="form-footer">
<a-button @click="closeModal" class="footer-btn cancel-btn">
<Icon icon="ant-design:close-circle-outlined" />
取消
</a-button>
<a-button type="primary" @click="nextStep" :disabled="!selectedPromptKey || !fieldMappings['user_query']" class="footer-btn next-btn">
<Icon icon="ant-design:right-circle-outlined" />
下一步
</a-button>
</div>
</div>
<!-- 第二步调试配置和结果显示 -->
<div v-show="currentStep === 1" class="step-content step-two">
<div class="form-section">
<div class="mapping-section">
<div class="section-header">
<h3>
<Icon icon="ant-design:link-outlined" />
字段映射
</h3>
</div>
<div class="mapping-list">
<div class="mapping-item" v-for="(item, index) in evaluatorFields" :key="index">
<div class="field-label">
<Icon icon="ant-design:tag-outlined" />
评估器 [{{ item.field }}]
</div>
<div class="equal-sign">=</div>
<a-select allow-clear v-model:value="fieldMappings[item.field]" placeholder="请选择数据集字段" class="field-select">
<a-select-option v-for="column in evaluatorColumns" :key="column.name" :value="column.name">
<span class="column-option">
<!-- <a-divider style="margin:4px 0;" v-if="column.label" />-->
<span :style="{color:column.label ? 'green' : 'blue'}"> [{{ column.label || '评测集' }}] </span>
<span>{{ column.name }}</span>
<span style="margin-left: 5px">
<a-tooltip :title="column.description">
<Icon icon="ant-design:info-circle-outlined" size="14"/>
</a-tooltip>
</span>
</span>
</a-select-option>
</a-select>
</div>
</div>
</div>
<div class="debug-result" v-if="debugResult.length > 0 || confirmLoading">
<div class="section-header">
<h3>
<Icon icon="ant-design:bar-chart-outlined" />
调试结果
</h3>
</div>
<div class="debug-result-container">
<!-- 条件渲染当有调试结果时显示表格 -->
<div class="result-table-container" v-if="debugResult.length > 0">
<!-- 添加外部容器和标题 -->
<div class="table-header">
<div class="table-title" v-if="!confirmLoading">调试结果</div>
<div class="table-title" v-if="confirmLoading"><span>实验初始化中,请稍后点击<a href="javascript:void(0)" @click="handleReload">刷新</a></span></div>
<div class="table-subtitle">实际输出与参考输出对比</div>
</div>
<!-- 表格区域 -->
<a-table
:columns="resultColumns"
:dataSource="debugResult"
:pagination="false"
:scroll="{ x: 'max-content' }"
class="result-table"
:rowClassName="setRowClassName"
>
<!-- 自定义单元格渲染 -->
<template #bodyCell="{ column, record, text }">
<!-- 状态列特殊样式 -->
<template v-if="column.dataIndex === 'status'">
<span :class="['status-badge', getStatusClass(record)]">
{{ text }}
</span>
</template>
</template>
</a-table>
</div>
<!-- 无数据时显示加载或提示 -->
<div v-else class="no-data-container">
<div class="loading-text" v-if="confirmLoading">
<loading-outlined style="font-size: 24px; margin-right: 8px" @click="handleReload" />
<span>实验初始化中请稍后点击<a href="javascript:void(0)" @click="handleReload">刷新</a></span>
</div>
<div class="empty-text" v-else> 暂无调试结果 </div>
</div>
</div>
</div>
</div>
<div class="form-footer">
<a-button @click="prevStep" class="footer-btn prev-btn">
<Icon icon="ant-design:left-circle-outlined" />
上一步
</a-button>
<a-button v-if="debugResult.length == 0" :disabled="canConfirmDebug" :loading="confirmLoading" type="primary" @click="confirmDebug" class="footer-btn confirm-btn">
<Icon v-if="!confirmLoading" icon="ant-design:play-circle-outlined" />
{{ confirmLoading ? '正在调试...' : '确认调试配置' }}
</a-button>
<a-button v-if="debugResult.length > 0" @click="closeModal" class="footer-btn close-btn">
<Icon icon="ant-design:check-circle-outlined" />
完成
</a-button>
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, reactive,computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import Icon from '/@/components/Icon/src/Icon.vue';
import { list, promptExperiment } from '../AiragPrompts.api';
import { useMessage } from '@/hooks/web/useMessage';
import { queryTrackById } from '@/views/super/airag/aiprompts/AiragExtData.api';
import { LoadingOutlined } from '@ant-design/icons-vue';
// Emits声明
const emit = defineEmits(['register', 'success']);
// 确认调试配置加载状态
const confirmLoading = ref(false);
// 提示信息
const { createMessage } = useMessage();
//核心数据
const record = ref<any>({});
// 步骤控制
const currentStep = ref(0);
// 选择调试的提示词
const selectedPromptKey = ref<string>('');
const selectedPrompt = ref<any>({});
// 评估器字段配置
const evaluatorFields = ref<any>([]);
// 评估器下拉列配置
const evaluatorColumns = ref<any>([]);
//字段映射
const fieldMappings = reactive<Record<string, string>>({});
//页码配置
const page = ref({
pageNo: 1,
pageSize: 10,
});
// 提示词选项数据
const promptOptions = ref<any[]>([]);
// 数据集列
const datasetColumns = ref<any[]>([]);
// 提示词参数
const promptParams = ref<any[]>([]);
// 调试结果
const debugResult = ref<any[]>([]);
// 结果表格列
const resultColumns = ref<any[]>([
{ title: '问题', dataIndex: 'userQuery', key: 'userQuery',fixed: 'left' },
{ title: '提示词输出答案', dataIndex: 'promptAnswer', key: 'promptAnswer',fixed: 'left' },
{ title: '评分', dataIndex: 'answerScore', key: 'answerScore',fixed: 'left' },
]);
// 确认调试配置
const canConfirmDebug = computed(() => {
let canConfirm = true;
evaluatorFields.value.forEach((field) => {
if(!fieldMappings[field.name]){
canConfirm = false;
}
});
return canConfirm;
});
// 表单赋值
const [registerModal, { closeModal }] = useModalInner(async (data) => {
//调试数据
record.value = data.record;
//重置数据
resetForm();
//查询提示词
await getPromptList();
//查询测评列配置
getDatasetColumns();
//查询评估器字段配置
getEvaluatorFields();
console.log('evaluatorColumns.value', evaluatorColumns.value);
});
/**
* 获取数据集列字段
*/
function getDatasetColumns() {
let datasetValue = record.value?.datasetValue;
if (datasetValue) {
let columns = JSON.parse(datasetValue).columns;
columns.forEach((item) => {
if(item.name !== 'action') {
datasetColumns.value.push({ ...item, key: item.name, title: item.name });
evaluatorColumns.value.push({ ...item });
if(!resultColumns.value.some(a=> a.dataIndex === item.name)){
resultColumns.value.push({ title: item.name, dataIndex: item.name, key: item.name });
}
}
});
} else {
createMessage.warning('未配置评测集信息!');
}
}
/**
* 获取数据集列字段
*/
function getEvaluatorFields() {
let dataValue = record.value?.dataValue;
if (dataValue) {
evaluatorFields.value = dataValue.match(/{{\s*([^}\s]+)\s*}}/g).map((match) => ({ field: match.replace(/{{\s*|\s*}}/g, '') }));
} else {
createMessage.warning('未配置评测集信息!');
}
//列配置
evaluatorColumns.value.push({ name: 'actual_output', label: '评测对象', description: '实际输出', dateType: 'String' });
}
/**
* 获取提示词
*/
async function getPromptList() {
const res = await list({ ...page });
if (res?.records) {
res?.records.forEach((item) => {
promptOptions.value.push({ name: item.name, key: item.promptKey, value: item.promptKey, ...item });
});
}
}
// 重置表单
function resetForm() {
currentStep.value = 0;
selectedPromptKey.value = '';
promptOptions.value = [];
datasetColumns.value = [];
promptParams.value = [];
evaluatorColumns.value = [];
Object.keys(fieldMappings).forEach((key) => {
delete fieldMappings[key];
});
debugResult.value = [];
}
// 提示词改变事件
function onPromptChange(value: string) {
//选中的提示词信息
selectedPrompt.value = promptOptions.value.find((item) => item.value == value);
// 清空之前的映射
Object.keys(fieldMappings).forEach((key) => {
delete fieldMappings[key];
});
let modelParam = selectedPrompt.value?.modelParam;
if(modelParam){
modelParam = typeof modelParam === 'string' ? JSON.parse(modelParam) : modelParam;
if(modelParam?.promptVariables && modelParam?.promptVariables.length > 0){
modelParam.promptVariables.forEach((item) => {
promptParams.value.push({ name: item.name, label: item.name, description: item.description, dateType: 'String' });
});
}
}
}
// 下一步
function nextStep() {
if (selectedPromptKey.value) {
currentStep.value = 1;
}
}
// 上一步
function prevStep() {
confirmLoading.value = false;
debugResult.value = [];
currentStep.value = 0;
}
// 确认调试配置
async function confirmDebug() {
// 模拟调试过程
let params = {
mappings: fieldMappings,
promptKey: selectedPromptKey.value,
extDataId: record.value.id,
};
console.log('开始调试...', params);
confirmLoading.value = true;
let res = await promptExperiment(params);
console.log('结束调试res...', res);
emit('success');
}
// 刷新评测集
async function handleReload() {
debugResult.value = []
let res = await queryTrackById({ id: record.value.id });
console.log('刷新评测集res...', res);
if (!res.success) {
createMessage.error(res.message);
return;
} else {
if (res.result && res.result.length > 0) {
console.log('刷新评测集res.result...', res.result);
confirmLoading.value = false;
// 1. 先找出最大version值
const maxVersion = Math.max(...res.result.map(item => item.version));
// 2. 过滤出所有具有最大version的项
const maxVersionItems = res.result.filter(item => item.version === maxVersion);
console.log('刷新评测集maxVersionItems...', maxVersionItems);
if (maxVersionItems.length > 0) {
maxVersionItems.forEach((item) => {
debugResult.value.push(JSON.parse(item.dataValue));
});
}
}else{
createMessage.warning('数据处理中,请稍后刷新!');
}
}
}
// 状态样式类
const getStatusClass = (record) => {
if (record.status === '成功') return 'status-success';
if (record.status === '失败') return 'status-failed';
if (record.status === '警告') return 'status-warning';
return '';
};
// 行样式
const setRowClassName = (record, index) => {
return index % 2 === 0 ? 'even-row' : 'odd-row';
};
</script>
<style lang="less" scoped>
@primary-color: #1890ff;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #ff4d4f;
@border-color: #e8e8e8;
@background-color: #f5f7fa;
@text-color: #262626;
@text-secondary: #8c8c8c;
.steps-container {
margin: 15px 0;
padding: 30px;
border-radius: 8px;
background: linear-gradient(90deg, #f0f8ff, #e6f7ff);
overflow: auto;
max-height: calc(100vh - 200px);
:deep(.ant-steps-item-icon) {
background: white;
border-color: @primary-color;
width: 35px;
.ant-steps-icon {
color: @primary-color;
}
}
:deep(.ant-steps-item-finish) {
.ant-steps-item-icon {
background: @primary-color;
width: 35px;
.ant-steps-icon {
color: white;
}
}
.ant-steps-item-title {
color: @primary-color;
}
}
:deep(.ant-steps-item-title) {
font-weight: 500;
}
}
.step-content {
padding: 20px;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
&.step-one {
.prompt-select {
width: 100%;
font-size: 14px;
}
}
&.step-two {
.mapping-summary {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
margin-top: 16px;
}
.summary-item {
display: flex;
padding: 12px 16px;
background-color: @background-color;
border-radius: 6px;
border: 1px solid @border-color;
.field-key {
font-weight: 500;
color: @text-color;
margin-right: 12px;
min-width: 80px;
&::after {
content: ':';
}
}
.field-value {
color: @text-secondary;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.section-header {
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: @text-color;
display: flex;
align-items: center;
gap: 8px;
svg {
color: @primary-color;
}
}
}
.form-section {
margin-bottom: 24px;
}
.form-item {
margin-bottom: 24px;
.form-label {
display: block;
margin-bottom: 10px;
font-weight: 500;
font-size: 15px;
color: @text-color;
}
.prompt-option {
display: flex;
align-items: center;
gap: 8px;
}
}
.mapping-section {
margin-top: 30px;
padding: 20px;
background-color: @background-color;
border-radius: 8px;
border: 1px solid @border-color;
.mapping-list {
.mapping-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid @border-color;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: @primary-color;
}
.field-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: @text-color;
min-width: 140px;
padding-right: 16px;
}
.equal-sign {
font-weight: bold;
color: @text-secondary;
margin: 0 12px;
}
.field-select {
flex: 1;
.column-option {
display: flex;
align-items: center;
gap: 8px;
}
}
}
}
}
.debug-result {
margin-top: 36px;
padding: 20px;
background-color: @background-color;
border-radius: 8px;
border: 1px solid @border-color;
.result-table-container {
margin-top: 16px;
border-radius: 6px;
overflow: hidden;
border: 1px solid @border-color;
:deep(.result-table) {
.ant-table-thead > tr > th {
background-color: #fafafa;
font-weight: 600;
color: @text-color;
}
.ant-table-tbody > tr:hover {
background-color: #f0f8ff;
}
}
}
}
.form-footer {
display: flex;
justify-content: flex-end;
gap: 16px;
padding-top: 24px;
margin-top: 24px;
border-top: 1px solid @border-color;
.footer-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
font-size: 14px;
border-radius: 6px;
transition: all 0.3s;
&.cancel-btn {
&:hover {
color: @error-color;
border-color: @error-color;
}
}
&.next-btn,
&.confirm-btn {
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.2);
}
}
&.prev-btn {
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
&.close-btn {
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(82, 196, 26, 0.2);
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.step-content {
padding: 16px;
}
.mapping-section {
padding: 16px;
.mapping-list {
.mapping-item {
flex-direction: column;
align-items: flex-start;
.field-label {
margin-bottom: 10px;
padding-right: 0;
}
.equal-sign {
display: none;
}
.field-select {
width: 100%;
margin-top: 8px;
}
}
}
}
.step-two .mapping-summary {
grid-template-columns: 1fr;
}
.form-footer {
flex-direction: column;
.footer-btn {
width: 100%;
justify-content: center;
}
}
}
.debug-result-container {
margin: 16px 0;
}
.table-header {
padding: 12px 16px;
background: #fafafa;
border: 1px solid #e8e8e8;
border-bottom: none;
border-radius: 4px 4px 0 0;
}
.table-title {
font-size: 16px;
font-weight: 600;
color: #1890ff;
}
.table-subtitle {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.result-table {
border: 1px solid #e8e8e8;
}
:deep(.ant-table-thead > tr > th) {
background-color: #f5f5f5;
font-weight: 600;
color: #333;
border-bottom: 2px solid #1890ff;
}
:deep(.ant-table-tbody > tr.even-row) {
background-color: #fafafa;
}
:deep(.ant-table-tbody > tr.odd-row) {
background-color: #fff;
}
:deep(.ant-table-tbody > tr:hover) {
background-color: #e6f7ff !important;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
}
.status-success {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.status-failed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
.status-warning {
background-color: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
.latency-text {
font-weight: 500;
}
.latency-low {
color: #52c41a;
}
.latency-medium {
color: #faad14;
}
.latency-high {
color: #ff4d4f;
}
.input-cell,
.output-cell {
padding: 4px 0;
}
.cell-label {
font-size: 11px;
color: #999;
margin-bottom: 2px;
}
.cell-value {
font-size: 13px;
color: #333;
word-break: break-word;
}
.output-cell.reference .cell-value {
color: #1890ff;
}
.output-cell.actual .cell-value {
color: #52c41a;
}
.table-footer {
display: flex;
justify-content: flex-start;
gap: 24px;
padding: 12px 16px;
background: #fafafa;
border: 1px solid #e8e8e8;
border-top: none;
border-radius: 0 0 4px 4px;
}
.footer-item {
display: flex;
align-items: center;
gap: 8px;
}
.footer-label {
font-size: 12px;
color: #666;
}
.footer-value {
font-size: 14px;
font-weight: 500;
color: #1890ff;
}
.no-data-container {
padding: 40px;
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 4px;
background-color: #fafafa;
}
.loading-text {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #666;
}
.empty-text {
color: #999;
}
</style>

View File

@@ -0,0 +1,917 @@
<template>
<div class="p-2">
<BasicModal
wrapClassName="ai-prompt-edit-modal"
destroyOnClose
@register="registerModal"
:canFullscreen="false"
defaultFullscreen
width="800px"
:footer="null"
@visible-change="visibleChange"
@cancel="handleCancel"
>
<template #title>
<div style="display: flex; width: 100%; justify-content: space-between; align-items: center">
<div style="display: flex">
<img :src="getImage()" class="header-img" />
<div class="header-name">{{ formState.name }}</div>
<a-tooltip title="提示词评估器">
<Icon icon="ant-design:edit-outlined" style="margin-left: 4px; cursor: pointer" color="#354052" size="20" @click="handleEdit"></Icon>
</a-tooltip>
</div>
<!-- <div>-->
<!-- 应用编排-->
<!-- <a-tooltip title="AI应用文档">-->
<!-- <a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">-->
<!-- <Icon style="position: relative; left: 2px; top: 1px" icon="ant-design:question-circle-outlined"></Icon>-->
<!-- </a>-->
<!-- </a-tooltip>-->
<!-- </div>-->
<div style="display: flex">
<a-button @click="handleOk" style="margin-right: 30px" type="primary">保存</a-button>
</div>
</div>
</template>
<div style="height: 100%; width: 100%">
<a-row :span="24">
<a-col :span="showTest?12:24">
<div class="orchestration">提示词评估器</div>
</a-col>
<a-col :span="12" v-if="showTest">
<div class="view">构造测试数据</div>
</a-col>
</a-row>
<a-row :span="24">
<a-col :span="showTest?12:24" class="setting-left">
<a-form class="antd-modal-form" ref="formRef" :model="formState" :rules="validatorRules">
<a-row>
<a-col :span="24">
<div class="prologue-chunk">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.modelId">
<template #label>
<div style="display: flex; justify-content: space-between; width: 100%; margin-right: 2px">
<div class="item-title">AI模型</div>
<div @click="handleParamSettingClick('model')" class="knowledge-txt">
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
</div>
</div>
</template>
<JDictSelectTag
v-model:value="formState.modelId"
placeholder="请选择AI模型"
dict-code="airag_model where model_type = 'LLM' and activate_flag = 1,name,id"
style="width: 100%"
@change="handleModelIdChange"
></JDictSelectTag>
</a-form-item>
</div>
</a-col>
<!-- 提示词 -->
<a-col :span="24" class="mt-10">
<div class="prompt-back">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.dataValue" style="margin-bottom: 0">
<template #label>
<div class="prompt-title-padding item-title space-between">
<span>评估器</span>
<a-button size="middle" ghost>
<span style="align-items: center; display: flex" @click="generatedPrompt">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"
/>
</svg>
<span style="margin-left: 4px">生成</span>
</span>
</a-button>
</div>
</template>
<a-textarea :rows="16" v-model:value="formState.dataValue" placeholder="请输入评估提示" />
<!-- 评估器内容格式 -->
<div class="variable-container">
<div class="variable-container-header">
<Icon icon="ant-design:file-text-outlined" class="output-format-icon" />
<span class="variable-format-title">评估器内容变量要求点击变量插入到评估器内容</span>
</div>
<div class="variable-container-content">
<div class="variable-tag-wrapper">
<a-tooltip title="评估的输入内容变量(必填)">
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('input')">input</a-tag>
</a-tooltip>
<a-tooltip title="评估的输出内容变量(必填)">
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('output')">output</a-tag>
</a-tooltip>
<a-tooltip title="评估的参考内容变量">
<a-tag color="default" class="variable-tag optional-tag" @click="handleTagClick('reference')">reference</a-tag>
</a-tooltip>
</div>
</div>
</div>
<!-- 输出格式 -->
<div class="output-format-card">
<div class="output-format-header">
<Icon icon="ant-design:file-text-outlined" class="output-format-icon" />
<span class="output-format-title">输出格式要求</span>
</div>
<div class="output-format-content">
<div class="output-item">
<!-- <div class="output-item-label">-->
<!-- <span class="output-item-title">得分</span>-->
<!-- </div>-->
<div class="output-item-desc">
得分最终的得分必须输出必须输出一个数字表示满足Prompt中评分标准的程度得分范围从
<span class="score-range">0.0</span> <span class="score-range">1.0</span><span class="score-range">1.0</span>
表示完全满足评分标准<span class="score-range">0.0</span> 表示完全不满足评分标准
</div>
</div>
<div class="output-item">
<div class="output-item-label">
<!-- <span class="output-item-bullet"></span>-->
<!-- <span class="output-item-title">原因</span>-->
</div>
<div class="output-item-desc">
原因对得分的可读解释最后必须用一句话结束理由该句话为因此应该给出的分数是你的评分
</div>
</div>
</div>
</div>
</a-form-item>
</div>
</a-col>
</a-row>
</a-form>
<a-button v-if="showTest" class="mt-10 ml" style="float: right" @click="showTest = false">取消</a-button>
<!-- <a-button class="mt-10" style="float: right" @click="showTest = true" type="primary">调试</a-button>-->
</a-col>
<a-col :span="12" class="setting-right" v-if="showTest">
<EvaluatorDebug ref="debugRef" :content="formState.dataValue" @run="debugRun"></EvaluatorDebug>
</a-col>
</a-row>
</div>
</BasicModal>
<!-- Ai配置弹窗 -->
<AiAppParamsSettingModal @register="registerParamsSettingModal" @ok="handleParamsSettingOk"></AiAppParamsSettingModal>
<!-- Ai生成器 -->
<AiAppGeneratedPromptModal @register="registerAiAppPromptModal" @ok="handleAiAppPromptOk"></AiAppGeneratedPromptModal>
<!-- Ai评估器弹窗 -->
<AiragExtDataModal @register="registerEvaluatorModal" @success="handleSuccess"></AiragExtDataModal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import {Form, message} from 'ant-design-vue';
import { defHttp } from '@/utils/http/axios';
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import AiAppParamsSettingModal from '@/views/super/airag/aiapp/components/AiAppParamsSettingModal.vue';
import AiAppGeneratedPromptModal from '@/views/super/airag/aiapp/components/AiAppGeneratedPromptModal.vue';
import AiragExtDataModal from './AiragExtDataModal.vue';
import EvaluatorDebug from './EvaluatorDebug.vue';
import defaultImg from '@/views/super/airag/aiapp/img/ailogo.png';
import { getFileAccessHttpUrl, randomString } from '@/utils/common/compUtils';
import { cloneDeep } from 'lodash-es';
import {debugEvaluator, saveOrUpdate} from '@/views/super/airag/aiprompts/AiragExtData.api';
//保存或修改
const isUpdate = ref<boolean>(false);
//uuid
const uuid = ref(randomString(16));
//showTest 显示调试器
const showTest = ref(true);
//debugRef 调试器引用
const debugRef = ref(null);
//form表单数据
const formState = reactive<any>({
id: '',
name: '',
dataValue: '',
descr: '',
modelId: '',
metadata: '',
});
//表单验证
const validatorRules = ref<any>({
dataValue: [{ required: true, message: '请输入提示词!' }],
modelId: [{ required: true, message: '请选择AI模型!' }],
});
//注册form
const useForm = Form.useForm;
const { resetFields, validate, validateInfos } = useForm(formState, validatorRules, { immediate: false });
const labelCol = ref<any>({ span: 24 });
const wrapperCol = ref<any>({ span: 24 });
//参数配置
const metadata = ref<any>({});
// emit
const emit = defineEmits(['success', 'register']);
//注册modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
clearFormState();
resetFields();
if (isUpdate.value) {
setFormState(data.record);
}
setModalProps({ bodyStyle: { padding: '10px' } });
});
//注册modal
const [registerParamsSettingModal, { openModal: paramsSettingOpen }] = useModal();
const [registerAiAppPromptModal, { openModal: aiAppPromptModalOpen }] = useModal();
const [registerEvaluatorModal, { openModal: evaluatorModalOpen }] = useModal();
//编辑
function handleEdit() {
evaluatorModalOpen(true, { isUpdate: true, showFooter: true, record: {
id: formState.id,
name: formState.name,
descr: formState.descr,
} });
}
/**
* 保存
*/
async function handleOk() {
try {
let values = await validate();
metadata.value.modelId = values.modelId;
values.metadata = JSON.stringify(cloneDeep(metadata.value));
values = Object.assign(formState, values);
//提交表单
await saveOrUpdate(values, isUpdate.value);
setModalProps({ confirmLoading: true });
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 取消
*/
function handleCancel() {
closeModal();
}
/**
* 标签点击事件
* @param type
*/
function handleTagClick(type) {
let label = type=='input'?'## 输入参数':type=='output'?'## 输出参数':'## 参考参数';
if(!formState.dataValue){
formState.dataValue = `${label}{{${type}}}`;
}else{
formState.dataValue += `\r\n\r\n${label}{{${type}}}`;
// 获取textarea元素并滚动到底部
setTimeout(() => {
const textarea = document.querySelector('textarea.ant-input') as HTMLTextAreaElement;
if (textarea) {
textarea.scrollTop = textarea.scrollHeight;
}
}, 0);
}
}
/**
* 关闭弹窗触发列表刷新
*
* @param value
*/
function visibleChange(value) {
if (!value) {
emit('success');
}
}
/**
* 成功回调
*/
function handleSuccess(data) {
setFormState(data);
emit('success');
}
/**
* 参数配置点击事件
* @param value
*/
function handleParamSettingClick(value) {
paramsSettingOpen(true, { type: value, metadata: metadata.value });
}
/**
* 参数配置确定回调事件
*
* @param value
*/
function handleParamsSettingOk(value) {
Object.assign(metadata.value, value);
}
/**
* 获取图标
*/
function getImage() {
return formState.icon ? getFileAccessHttpUrl(formState.icon) : defaultImg;
}
/**
* 清除参数
*/
function clearFormState() {
formState.id = '';
formState.name = '';
formState.dataValue = '';
formState.descr = '';
formState.modelId = '';
formState.metadata = '';
}
/**
* 设置form属性
* @param data
*/
function setFormState(data: any) {
//赋值
Object.assign(formState, data);
// 如果已有metadata查询模型信息并更新到metadata中
if (data?.metadata) {
metadata.value = data.metadata ? JSON.parse(data.metadata) : {};
formState.modelId = data.metadata ? JSON.parse(data.metadata).modelId || '' : '';
}
}
//============= begin 提示词 ================================
/**
* 生成提示词
*/
function generatedPrompt() {
aiAppPromptModalOpen(true, {});
}
/**
* 提示词回调
*
* @param value
*/
function handleAiAppPromptOk(value) {
formState.dataValue = value;
}
//============= end 提示词 ================================
/**
* 调试运行
*/
async function debugRun(variables: any[]) {
//提示词
let sysMessage = formState.dataValue;
let userMessage = "输入的内容:";
//替换变量
variables.forEach((item) => {
userMessage += `${item.name}:${item.value}`;
});
//定义返回结果
sysMessage += '定义返回格式:\n\n 得分最终的得分必须输出必须输出一个数字表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.01.0 表示完全满足评分标准0.0 表示完全不满足评分标准。\n' +
'原因:(对得分的可读解释)。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是(你前面得出的评分)。';
console.log('userMessage', userMessage);
if(!formState.modelId) {
message.warning("请选择AI模型");
return;
}
// 调用调试器运行
let loading = debugRef.value?.loading;
if (loading) return;
debugRef.value.loading = true;
let params = { prompts: sysMessage,content:userMessage,modelId:formState.modelId,modelParam:JSON.stringify(formState.metadata) }
let res = await debugEvaluator(params).catch(() => debugRef.value.loading = false);
debugRef.value.loading = false;
if(res.success){
debugRef.value.result = res.result
}else{
message.error(res.message);
}
console.log("debugEvaluator",res)
}
/**
* 模型ID变化处理
* 查询模型信息并更新到metadata中供chat组件使用
*/
async function handleModelIdChange(modelId: string) {
if (!modelId) {
// 如果清空模型,清除模型信息
if (metadata.value.modelInfo) {
delete metadata.value.modelInfo;
}
return;
}
try {
const res = await defHttp.get(
{
url: '/airag/airagModel/queryById',
params: { id: modelId },
},
{ isTransformResponse: false }
);
if (res.success && res.result) {
const model = res.result;
// 将模型信息添加到metadata中
if (!metadata.value) {
metadata.value = {};
}
metadata.value['modelInfo'] = {
provider: model.provider || '',
modelType: model.modelType || '',
modelName: model.modelName || '',
};
}
} catch (e) {
console.error('获取模型信息失败', e);
}
}
</script>
<style scoped lang="less">
.pointer {
cursor: pointer;
}
.orchestration,
.view {
color: #0a3069;
font-weight: bold;
text-align: center;
font-size: 18px;
width: 100%;
}
.type-title {
color: #1d2025;
margin-bottom: 4px;
}
.type-desc {
color: #8f959e;
font-weight: 400;
}
.setting-left {
padding: 20px;
overflow-y: auto;
height: (100vh - 15px);
}
.setting-right {
overflow-y: auto;
height: (100vh - 15px);
border-left: 1px solid #dee0e3;
padding: 12px;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-form-item .ant-form-item-label > label) {
width: 100%;
}
.knowledge-img {
width: 30px;
height: 30px;
}
.flow-name {
font-size: 14px;
font-weight: bold;
color: #354052;
width: calc(100% - 20px);
overflow: hidden;
align-content: center;
text-overflow: ellipsis;
white-space: nowrap;
display: grid;
}
.knowledge-name {
margin-left: 4px;
}
.knowledge-card {
margin-bottom: 10px;
margin-right: 10px;
}
.knowledge-icon {
display: none !important;
position: relative;
top: 6px;
}
.knowledge-card:hover {
.knowledge-icon {
display: block !important;
}
}
.header-img {
width: 35px;
height: 35px;
border-radius: 10px;
}
.flex {
display: flex;
}
.header-name {
color: #354052;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 300px;
margin-left: 10px;
align-content: center;
}
.prompt-back {
background-color: #eef4ff;
border-radius: 12px;
padding: 2px;
border: 1px solid #77b2f8;
box-sizing: border-box;
margin-left: 5px;
textarea {
min-height: 250px;
max-height: 400px;
border: none !important;
}
}
.prompt-title-padding {
margin-left: 14px;
height: 50px;
align-content: center;
}
.prologue-chunk {
background-color: #f2f4f7;
border-radius: 12px;
padding: 2px 10px 2px 10px;
box-sizing: border-box;
}
.prologue-chunk-edit {
background-color: #f2f4f7;
border-radius: 12px;
padding: 2px 0 2px 0;
box-sizing: border-box;
}
.mt-10 {
margin-top: 10px;
}
:deep(.ant-form-item-label) {
padding: 0 !important;
}
:deep(.ant-form-item-required) {
margin-left: 4px !important;
}
.knowledge-txt {
color: #354052;
cursor: pointer;
margin-right: 10px;
font-size: 12px;
}
.item-title {
color: #111928;
font-weight: 400;
}
:deep(.ant-form-item) {
margin-bottom: 5px;
}
:deep(.vditor) {
border: none;
}
:deep(.vditor-sv) {
font-size: 14px;
}
:deep(.vditor-sv:focus) {
background-color: #ffffff;
}
.space-between {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
button {
padding: 0 6px;
height: 25px;
color: #155aef !important;
margin-right: 10px;
border: none;
}
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.quick-command {
display: flex;
width: 100%;
margin-top: 10px;
justify-content: space-between;
background-color: #ffffff;
padding: 4px 8px 4px;
align-items: center;
align-content: center;
align-self: center;
border-radius: 8px;
height: 40px;
.quick-command-icon {
display: none;
}
}
.quick-command:hover {
background-color: #eff0f8;
.quick-command-icon {
display: flex;
}
}
.data-empty-text {
color: #757c8f;
margin-left: 10px;
}
.mcp-warning-tip {
display: flex;
align-items: center;
color: #fa8c16;
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
.flow-icon {
width: 34px;
height: 34px;
border-radius: 10px;
}
:deep(.ant-card .ant-card-body) {
padding: 16px;
}
.text-status {
font-size: 12px;
color: #676f83;
}
.tag-text {
display: flow;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 20px;
font-size: 12px;
color: #3a3f4f;
}
.tag-input {
align-self: center;
color: #737c97;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
margin-right: 6px;
text-align: right;
white-space: nowrap;
}
.tags-meadata {
padding-inline: 2px;
border-radius: 4px;
display: flex;
font-weight: 500;
max-width: 100%;
}
.text-desc {
width: 100%;
font-weight: 400;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
font-size: 12px;
color: #676f83;
}
/* 输出格式卡片样式 */
.output-format-card {
background: #f8f9ff;
border: 1px solid #e1e5ff;
border-radius: 8px;
padding: 16px;
margin-top: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.output-format-header {
display: flex;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #e8ecff;
padding-bottom: 8px;
}
.output-format-icon {
color: #155aef;
font-size: 16px;
margin-right: 8px;
}
.output-format-title {
font-weight: 600;
color: #155aef;
font-size: 14px;
}
.output-format-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.output-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.output-item-label {
display: flex;
align-items: center;
gap: 6px;
}
.output-item-bullet {
color: #155aef;
font-weight: bold;
font-size: 14px;
}
.output-item-title {
font-weight: 600;
color: #354052;
font-size: 13px;
}
.output-item-desc {
color: #5a6376;
font-size: 13px;
line-height: 1.5;
margin-left: 20px;
}
.score-range {
color: #e74c3c;
font-weight: 600;
background: #fff5f5;
padding: 1px 4px;
border-radius: 3px;
font-size: 12px;
}
.output-format-card {
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
margin-top: 16px;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
.output-format-header {
display: flex;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
.output-format-icon {
font-size: 16px;
color: #1890ff;
margin-right: 8px;
}
.output-format-title {
font-size: 14px;
font-weight: 500;
color: #262626;
}
}
.output-format-content {
.output-item {
margin-bottom: 12px;
padding: 8px 12px;
background: #fff;
border-radius: 4px;
border-left: 3px solid #1890ff;
&:last-child {
margin-bottom: 0;
}
.output-item-desc {
font-size: 13px;
color: #595959;
line-height: 1.6;
.score-range {
color: #ff7a45;
font-weight: 600;
background: rgba(255, 122, 69, 0.1);
padding: 2px 6px;
border-radius: 4px;
margin: 0 2px;
}
}
}
}
}
.variable-container {
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
margin-top: 16px;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
.variable-container-header {
display: flex;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
.output-format-icon {
font-size: 16px;
color: #1890ff;
margin-right: 8px;
}
.variable-format-title {
font-size: 14px;
font-weight: 500;
color: #262626;
}
}
.variable-container-content {
.variable-tag-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
.variable-tag {
cursor: pointer;
border-radius: 4px;
font-size: 12px;
padding: 4px 8px;
margin: 0;
transition: all 0.3s ease;
&.required-tag {
background-color: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
}
&.optional-tag {
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
color: #8c8c8c;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
}
}
}
}
</style>
<style lang="less">
.ai-prompt-edit-modal {
.ant-modal .ant-modal-header {
padding: 13px 32px !important;
}
.jeecg-basic-modal-close > span {
margin-left: 0;
}
}
</style>

View File

@@ -0,0 +1,955 @@
<template>
<div class="p-2">
<BasicModal
wrapClassName="ai-prompt-edit-modal"
destroyOnClose
@register="registerModal"
:canFullscreen="false"
defaultFullscreen
width="800px"
:footer="null"
@visible-change="visibleChange"
@cancel="handleCancel"
>
<template #title>
<div style="display: flex; width: 100%; justify-content: space-between; align-items: center">
<div style="display: flex">
<img :src="getImage()" class="header-img" />
<div class="header-name">{{ formState.name }}</div>
<a-tooltip title="编辑">
<Icon icon="ant-design:edit-outlined" style="margin-left: 4px; cursor: pointer" color="#354052" size="20" @click="handleEdit"></Icon>
</a-tooltip>
</div>
<div>
提示词编排
<a-tooltip title="AI应用文档">
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">
<Icon style="position: relative; left: 2px; top: 1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</div>
<div style="display: flex">
<a-button @click="handleOk" style="margin-right: 30px" type="primary">保存</a-button>
</div>
</div>
</template>
<div style="height: 100%; width: 100%">
<a-row :span="24">
<a-col :span="10">
<div class="orchestration">提示词</div>
</a-col>
<a-col :span="14">
<div class="view">预览</div>
</a-col>
</a-row>
<a-row :span="24">
<a-col :span="10" class="setting-left">
<a-form class="antd-modal-form" ref="formRef" :model="formState" :rules="validatorRules">
<a-row>
<a-col :span="24">
<div class="prompt-back">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.content" style="margin-bottom: 0">
<template #label>
<div class="prompt-title-padding item-title space-between">
<span>提示词</span>
<a-button size="middle" ghost>
<span style="align-items: center; display: flex" @click="generatedPrompt">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"
/>
</svg>
<span style="margin-left: 4px">生成</span>
</span>
</a-button>
</div>
</template>
<a-textarea :rows="8" v-model:value="formState.content" placeholder="请输入提示词" @change="handleContentChange" />
</a-form-item>
</div>
</a-col>
<a-col :span="24" class="mt-10">
<div class="prologue-chunk">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.modelId">
<template #label>
<div style="display: flex; justify-content: space-between; width: 100%; margin-right: 2px">
<div class="item-title">AI模型</div>
<div @click="handleParamSettingClick('model')" class="knowledge-txt">
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
</div>
</div>
</template>
<JDictSelectTag
v-model:value="formState.modelId"
placeholder="请选择AI模型"
dict-code="airag_model where model_type = 'LLM' and activate_flag = 1,name,id"
style="width: 100%"
@change="handleModelIdChange"
></JDictSelectTag>
</a-form-item>
</div>
</a-col>
<a-col :span="24" class="mt-10" v-if="metadata.promptVariables && metadata.promptVariables.length > 0">
<div class="prologue-chunk">
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol">
<template #label>
<div style="display: flex; justify-content: space-between; width: 100%; margin-right: 2px">
<div class="item-title">Prompt 变量</div>
</div>
</template>
<!-- 变量列表容器 -->
<div class="prompt-variable-container">
<!-- 无变量时的空状态 -->
<div v-if="!metadata.promptVariables || metadata.promptVariables.length === 0" class="empty-variables">
<Icon icon="ant-design:inbox-outlined" style="font-size: 32px; color: #d9d9d9" />
<p>暂无变量</p>
<span class="empty-hint">请在提示词中使用 {{ 变量名 }} 格式添加变量</span>
</div>
<!-- 变量列表 -->
<div v-else class="prompt-variables-list">
<div
class="prompt-variable-item"
v-for="(item, index) in metadata.promptVariables"
:key="index"
:class="{ 'variable-focused': focusedVariable === index }"
>
<!-- 变量名标签 -->
<div class="variable-tag">
<Icon icon="ant-design:tag-outlined" />
<span class="variable-name">{{ item.name }}</span>
</div>
<!-- 变量值输入框 -->
<div class="variable-input-wrapper">
<!-- 附件输入框 -->
<JImageUpload :maxCount="1" v-if="item.type === 'FILE'" v-model:value="item.value"></JImageUpload>
<!-- 文本输入框 -->
<a-input
v-else
v-model:value="item.value"
placeholder="请输入变量值"
@focus="focusedVariable = index"
@blur="focusedVariable = null"
class="variable-input"
>
<template #suffix>
<Icon v-if="item.value" icon="ant-design:check-circle-outlined" style="color: #52c41a" />
</template>
</a-input>
</div>
<!-- 类型选择框 -->
<div class="variable-input-wrapper">
<a-select
v-model:value="item.type"
placeholder="请选择类型"
:options="[
{ value: 'TEXT', label: '文本' },
{ value: 'FILE', label: '附件' }
]"
class="variable-input"
>
</a-select>
</div>
<!-- 变量操作按钮 -->
<div class="variable-actions">
<a-tooltip title="清空值">
<a-button type="text" size="small" @click="item.value = ''" class="action-btn">
<Icon icon="ant-design:clear-outlined" />
</a-button>
</a-tooltip>
</div>
</div>
</div>
</div>
</a-form-item>
</div>
</a-col>
</a-row>
</a-form>
</a-col>
<a-col :span="14" class="setting-right">
<chat :uuid="uuid" :formState="chatData" url="/airag/app/debug"></chat>
</a-col>
</a-row>
</div>
</BasicModal>
<!-- Ai配置弹窗 -->
<AiAppParamsSettingModal @register="registerParamsSettingModal" @ok="handleParamsSettingOk"></AiAppParamsSettingModal>
<!-- Ai生成器 -->
<AiAppGeneratedPromptModal @register="registerAiAppPromptModal" @ok="handleAiAppPromptOk"></AiAppGeneratedPromptModal>
<!-- Ai提示词弹窗 -->
<AiragPromptsModal @register="registerPromptModal" @success="handleSuccess"></AiragPromptsModal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { Form } from 'ant-design-vue';
import { defHttp } from '@/utils/http/axios';
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import AiAppParamsSettingModal from '@/views/super/airag/aiapp/components/AiAppParamsSettingModal.vue';
import AiAppGeneratedPromptModal from '@/views/super/airag/aiapp/components/AiAppGeneratedPromptModal.vue';
import AiragPromptsModal from './AiragPromptsModal.vue';
import chat from '@/views/super/airag/aiapp/chat/chat.vue';
import defaultImg from '@/views/super/airag/aiapp/img/ailogo.png';
import { getFileAccessHttpUrl, randomString } from '@/utils/common/compUtils';
import { cloneDeep } from 'lodash-es';
import { saveOrUpdate } from '@/views/super/airag/aiprompts/AiragPrompts.api';
import { JImageUpload } from "@/components/Form";
//保存或修改
const isUpdate = ref<boolean>(false);
//uuid
const uuid = ref(randomString(16));
//form表单数据
const formState = reactive<any>({
id: '',
name: '',
promptKey: '',
content: '',
description: '',
modelId: '',
modelParam: '',
});
//表单验证
const validatorRules = ref<any>({
content: [{ required: true, message: '请输入提示词!' }],
modelId: [{ required: true, message: '请选择AI模型!' }],
});
//注册form
const useForm = Form.useForm;
const { resetFields, validate, validateInfos } = useForm(formState, validatorRules, { immediate: false });
const labelCol = ref<any>({ span: 24 });
const wrapperCol = ref<any>({ span: 24 });
//参数配置
const metadata = ref<any>({});
//定义 emit 事件
const emit = defineEmits(['success', 'register']);
// 添加聚焦变量索引
const focusedVariable = ref<number | null>(null);
// 提示词
const prompt = ref<any>();
//注册modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
clearFormState();
resetFields();
if (isUpdate.value) {
setFormState(data.record);
}
setModalProps({ bodyStyle: { padding: '10px' } });
});
//注册modal
const [registerParamsSettingModal, { openModal: paramsSettingOpen }] = useModal();
const [registerAiAppPromptModal, { openModal: aiAppPromptModalOpen }] = useModal();
const [registerPromptModal, { openModal: promptModalOpen }] = useModal();
const chatData = computed(() => {
return {
id: formState.id,
name: formState.name,
prompt: prompt.value,
modelId: formState.modelId,
metadata: metadata.value?.modelInfo ? JSON.stringify(metadata.value.modelInfo) : '',
};
});
watch(
() => metadata.value,
() => {
changePrompt();
},
{
deep: true,
}
);
/**
* 改变提示词
*/
function changePrompt() {
prompt.value = formState.content;
if (metadata.value && metadata.value['promptVariables']) {
metadata.value['promptVariables'].forEach((variable) => {
if (variable.type === 'FILE') {
variable.value = getFileAccessHttpUrl(variable.value);
}
prompt.value = prompt.value.replace(new RegExp(`{{${variable.name}}}`, 'g'), variable.value);
});
}
}
/**
* 提示词内容改变事件
*/
function handleContentChange() {
if (formState.content) {
console.log("formState.content",formState.content)
let variables = extractVariables(formState.content);
console.log("variables",variables)
if(variables.length > 0){
const promptVariables = metadata.value['promptVariables'];
metadata.value['promptVariables'] = variables.map((variable) => ({
name: variable,
value: promptVariables.find((item) => item.name === variable)?.value || '',
type: promptVariables.find((item) => item.name === variable)?.type || 'TEXT'
}));
}else{
metadata.value['promptVariables'] = '';
}
} else {
metadata.value['promptVariables'] = '';
}
}
/**
* 从提示词中提取变量名
* @param str
*/
function extractVariables(str) {
// 匹配 {{ 和 }} 之间的内容,非贪婪匹配
const regex = /\{\{(.*?)\}\}/g;
const matches = new Set(); // 使用 Set 自动去重
let match;
while ((match = regex.exec(str)) !== null) {
matches.add(match[1].trim()); // 去除可能的首尾空格并添加到 Set
}
return Array.from(matches);;
}
//编辑
function handleEdit() {
promptModalOpen(true, {
isUpdate: true,
showFooter: true,
record: {
id: formState.id,
name: formState.name,
promptKey: formState.promptKey,
description: formState.description,
},
});
}
/**
* 保存
*/
async function handleOk() {
try {
let values = await validate();
values.modelParam = JSON.stringify(cloneDeep(metadata.value));
values = Object.assign(formState, values);
//提交表单
await saveOrUpdate(values, isUpdate.value);
setModalProps({ confirmLoading: true });
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 取消
*/
function handleCancel() {
closeModal();
}
/**
* 关闭弹窗触发列表刷新
*
* @param value
*/
function visibleChange(value) {
if (!value) {
emit('success');
}
}
/**
* 成功回调
*/
function handleSuccess(data) {
setFormState(data);
emit('success');
}
/**
* 参数配置点击事件
* @param value
*/
function handleParamSettingClick(value) {
paramsSettingOpen(true, { type: value, metadata: metadata.value });
}
/**
* 参数配置确定回调事件
*
* @param value
*/
function handleParamsSettingOk(value) {
Object.assign(metadata.value, value);
}
/**
* 获取图标
*/
function getImage() {
return formState.icon ? getFileAccessHttpUrl(formState.icon) : defaultImg;
}
/**
* 清除参数
*/
function clearFormState() {
formState.id = '';
formState.name = '';
formState.content = '';
formState.description = '';
formState.modelId = '';
formState.modelParam = '';
metadata.value = {};
prompt.value = '';
}
/**
* 设置form属性
* @param data
*/
function setFormState(data: any) {
//赋值
Object.assign(formState, data);
// 如果已有modelId查询模型信息并更新到metadata中
if (data.modelId) {
metadata.value = data.modelParam ? JSON.parse(data.modelParam) : {};
}
console.log('设置form属性formState', formState);
}
//============= begin 提示词 ================================
/**
* 生成提示词
*/
function generatedPrompt() {
aiAppPromptModalOpen(true, {});
}
/**
* 提示词回调
*
* @param value
*/
function handleAiAppPromptOk(value) {
console.log('handleAiAppPromptOk value', value);
formState.content = value;
}
//============= end 提示词 ================================
/**
* 模型ID变化处理
* 查询模型信息并更新到metadata中供chat组件使用
*/
async function handleModelIdChange(modelId: string) {
if (!modelId) {
// 如果清空模型,清除模型信息
if (metadata.value.modelInfo) {
delete metadata.value.modelInfo;
}
return;
}
try {
const res = await defHttp.get(
{
url: '/airag/airagModel/queryById',
params: { id: modelId },
},
{ isTransformResponse: false }
);
if (res.success && res.result) {
const model = res.result;
// 将模型信息添加到metadata中
if (!metadata.value) {
metadata.value = {};
}
metadata.value['modelInfo'] = {
provider: model.provider || '',
modelType: model.modelType || '',
modelName: model.modelName || '',
};
}
} catch (e) {
console.error('获取模型信息失败', e);
}
}
</script>
<style scoped lang="less">
.pointer {
cursor: pointer;
}
.orchestration,
.view {
color: #0a3069;
font-weight: bold;
text-align: center;
font-size: 18px;
width: 100%;
padding-bottom: 10px;
}
.type-title {
color: #1d2025;
margin-bottom: 4px;
}
.type-desc {
color: #8f959e;
font-weight: 400;
}
.setting-left {
padding: 20px;
overflow-y: auto;
height: (100vh - 15px);
}
.setting-right {
overflow-y: auto;
height: (100vh - 15px);
border-left: 1px solid #dee0e3;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-form-item .ant-form-item-label > label) {
width: 100%;
}
.knowledge-img {
width: 30px;
height: 30px;
}
.flow-name {
font-size: 14px;
font-weight: bold;
color: #354052;
width: calc(100% - 20px);
overflow: hidden;
align-content: center;
text-overflow: ellipsis;
white-space: nowrap;
display: grid;
}
.knowledge-name {
margin-left: 4px;
}
.knowledge-card {
margin-bottom: 10px;
margin-right: 10px;
}
.knowledge-icon {
display: none !important;
position: relative;
top: 6px;
}
.knowledge-card:hover {
.knowledge-icon {
display: block !important;
}
}
.header-img {
width: 35px;
height: 35px;
border-radius: 10px;
}
.flex {
display: flex;
}
.header-name {
color: #354052;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 300px;
margin-left: 10px;
align-content: center;
}
.prompt-back {
background-color: #eef4ff;
border-radius: 12px;
padding: 2px;
border: 1px solid #77b2f8;
box-sizing: border-box;
margin-left: 5px;
textarea {
min-height: 250px;
max-height: 400px;
border: none !important;
}
}
.prompt-title-padding {
margin-left: 14px;
height: 50px;
align-content: center;
}
.prologue-chunk {
background-color: #f2f4f7;
border-radius: 12px;
padding: 2px 10px 2px 10px;
box-sizing: border-box;
}
.prologue-chunk-edit {
background-color: #f2f4f7;
border-radius: 12px;
padding: 2px 0 2px 0;
box-sizing: border-box;
}
.mt-10 {
margin-top: 10px;
}
:deep(.ant-form-item-label) {
padding: 0 !important;
}
:deep(.ant-form-item-required) {
margin-left: 4px !important;
}
.knowledge-txt {
color: #354052;
cursor: pointer;
margin-right: 10px;
font-size: 12px;
}
.item-title {
color: #111928;
font-weight: 400;
}
:deep(.ant-form-item) {
margin-bottom: 5px;
}
:deep(.vditor) {
border: none;
}
:deep(.vditor-sv) {
font-size: 14px;
}
:deep(.vditor-sv:focus) {
background-color: #ffffff;
}
.space-between {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
button {
padding: 0 6px;
height: 25px;
color: #155aef !important;
margin-right: 10px;
border: none;
}
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.quick-command {
display: flex;
width: 100%;
margin-top: 10px;
justify-content: space-between;
background-color: #ffffff;
padding: 4px 8px 4px;
align-items: center;
align-content: center;
align-self: center;
border-radius: 8px;
height: 40px;
.quick-command-icon {
display: none;
}
}
.quick-command:hover {
background-color: #eff0f8;
.quick-command-icon {
display: flex;
}
}
.data-empty-text {
color: #757c8f;
margin-left: 10px;
}
.mcp-warning-tip {
display: flex;
align-items: center;
color: #fa8c16;
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
.flow-icon {
width: 34px;
height: 34px;
border-radius: 10px;
}
:deep(.ant-card .ant-card-body) {
padding: 16px;
}
.text-status {
font-size: 12px;
color: #676f83;
}
.tag-text {
display: flow;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 20px;
font-size: 12px;
color: #3a3f4f;
}
.tag-input {
align-self: center;
color: #737c97;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
margin-right: 6px;
text-align: right;
white-space: nowrap;
}
.tags-meadata {
padding-inline: 2px;
border-radius: 4px;
display: flex;
font-weight: 500;
max-width: 100%;
}
.text-desc {
width: 100%;
font-weight: 400;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
font-size: 12px;
color: #676f83;
}
// 变量信息样式
.variable-info {
display: flex;
align-items: center;
color: #676f83;
font-size: 12px;
font-weight: 500;
}
// 变量容器样式
.prompt-variable-container {
margin-top: 12px;
min-height: 80px;
}
// 空状态样式
.empty-variables {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30px 20px;
background: #fafafa;
border-radius: 8px;
border: 1px dashed #d9d9d9;
p {
margin: 12px 0 8px;
font-size: 16px;
color: #676f83;
}
.empty-hint {
font-size: 13px;
color: #8c8c8c;
}
}
// 变量列表样式
.prompt-variables-list {
display: flex;
flex-direction: column;
gap: 12px;
}
// 变量项样式
.prompt-variable-item {
display: flex;
align-items: center;
padding: 16px;
background: white;
border-radius: 8px;
border: 1px solid #e8e8e8;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
// 悬停效果
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: #1890ff;
transform: translateY(-2px);
}
// 聚焦效果
&.variable-focused {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
// 左侧装饰条
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: #1890ff;
opacity: 0;
transition: opacity 0.3s;
}
&:hover::before,
&.variable-focused::before {
opacity: 1;
}
}
// 变量标签样式
.variable-tag {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
padding: 6px 12px;
background: #f0f8ff;
border-radius: 6px;
border: 1px solid #b7e3ff;
margin-right: 16px;
.variable-name {
font-weight: 600;
color: #1890ff;
font-size: 14px;
}
}
// 变量输入框包装器
.variable-input-wrapper {
flex: 1;
margin-right: 12px;
.variable-input {
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
}
&:focus,
&:focus-within {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
}
}
// 变量操作按钮
.variable-actions {
display: flex;
gap: 4px;
opacity: 0.7;
transition: opacity 0.3s;
.action-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: #676f83;
transition: all 0.2s;
&:hover {
background: #f0f8ff;
color: #1890ff;
transform: scale(1.05);
}
}
}
// 变量项悬停时显示操作按钮
.prompt-variable-item:hover .variable-actions {
opacity: 1;
}
// 响应式设计
@media (max-width: 768px) {
.prompt-variable-item {
flex-direction: column;
align-items: flex-start;
padding: 12px;
.variable-tag {
margin-right: 0;
margin-bottom: 10px;
width: 100%;
}
.variable-input-wrapper {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
.variable-actions {
width: 100%;
justify-content: flex-end;
opacity: 1;
}
}
}
</style>
<style lang="less">
.ai-prompt-edit-modal {
.ant-modal .ant-modal-header {
padding: 13px 32px !important;
}
.jeecg-basic-modal-close > span {
margin-left: 0;
}
}
</style>

View File

@@ -0,0 +1,364 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:canFullscreen="true"
destroyOnClose
title="列配置"
okText="保存"
:width="1000"
@ok="handleSubmit"
>
<div class="dynamic-column-config">
<a-card size="small">
<!-- 列配置列表 -->
<div class="column-list">
<div v-for="(element, index) in columns" :key="element.id" class="column-item">
<!-- 操作区域 - 放在右上角 -->
<div class="action-area">
<a-button type="text" @click="handleCopyColumn(element)" title="复制">
<copy-outlined />
</a-button>
<a-button type="text" danger @click="handleRemoveColumn(index)" :disabled="columns.length < 2" title="删除">
<DeleteOutlined />
</a-button>
</div>
<!-- 列配置表单 -->
<div class="column-form">
<div class="form-row">
<div class="form-item">
<label class="form-label">名称</label>
<a-input
v-model:value="element.name"
placeholder="请输入列名称"
:class="{ 'has-error': !element.name }"
@blur="validateColumn(element)"
/>
</div>
<div class="form-item">
<label class="form-label">数据类型</label>
<a-select v-model:value="element.dataType" placeholder="请选择数据类型" style="width: 100%">
<a-select-option value="String">字符串</a-select-option>
<a-select-option value="FILE">附件</a-select-option>
</a-select>
</div>
<div class="form-item">
<label class="form-label">必填</label>
<a-radio-group v-model:value="element.required">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</div>
</div>
<div class="form-row">
<div class="form-item full-width">
<label class="form-label">描述</label>
<a-textarea v-model:value="element.description" placeholder="请输入列描述" :rows="2" />
</div>
</div>
</div>
</div>
<a-button type="dashed" style="width: 100%" @click="handleAddColumn"> <PlusOutlined />添加列 </a-button>
</div>
<!-- 底部操作按钮 -->
<!-- <div class="footer-actions">-->
<!-- <a-button type="primary" @click="handleSubmit" :loading="loading"> 创建 </a-button>-->
<!-- <a-button @click="handleReset" style="margin-left: 8px"> 重置 </a-button>-->
<!-- </div>-->
</a-card>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { message } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons-vue';
import { saveOrUpdate } from '../AiragExtData.api';
import {JImageUpload} from "@/components/Form";
// Emits声明
const emit = defineEmits(['register', 'success']);
// 列配置接口
interface ColumnConfig {
id: string;
name: string;
description: string;
dataType: string;
required: boolean;
}
// 响应式数据
const loading = ref(false);
const columns = ref<ColumnConfig[]>([
{
id: generateId(),
name: '',
description: '',
dataType: 'String',
required: false,
},
]);
const defaultColumns = [
{
id: generateId(),
name: 'input',
description: '作为输入投递给评测对象',
dataType: 'String',
required: false,
},
{
id: generateId(),
name: 'reference_output',
description: '预期理想输出,可作为评估时的参考标准',
dataType: 'String',
required: false,
},
];
//数据ID
const dataId = ref('');
const datasetValue = ref<any>({});
// 生成唯一ID
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
//表单赋值
const [registerModal, { closeModal }] = useModalInner(async (data) => {
console.log('data:', data);
dataId.value = data.id;
if(data.datasetValue){
datasetValue.value = data.datasetValue
if(data.datasetValue?.columns && data.datasetValue.columns.length){
columns.value = data.datasetValue.columns;
}else{
columns.value = defaultColumns;
}
}
});
// 添加列
const handleAddColumn = () => {
columns.value.push({
id: generateId(),
name: '',
description: '',
dataType: 'String',
required: false,
});
};
// 删除列
const handleRemoveColumn = (index: number) => {
if (columns.value.length > 1) {
columns.value.splice(index, 1);
}
};
// 复制列
const handleCopyColumn = (element) => {
columns.value.push({
...element,
id: generateId(),
});
};
// 验证列配置
const validateColumn = (column: ColumnConfig) => {
return column.name && column.name.trim() !== '';
};
// 提交配置
const handleSubmit = async () => {
// 验证数据
const invalidColumns = columns.value.filter((col) => !validateColumn(col));
if (invalidColumns.length > 0) {
message.error('请填写所有必填项(名称)');
return;
}
loading.value = true;
try {
datasetValue.value.columns = columns.value
// 构造提交数据
const submitData = {
datasetValue: JSON.stringify( datasetValue.value),
id: dataId.value,
};
console.log('提交数据:', submitData);
await saveOrUpdate(submitData, true,false);
message.success('配置创建成功!');
// 关闭弹窗并触发成功事件
closeModal();
emit('success');
} catch (error) {
console.error('提交失败:', error);
} finally {
loading.value = false;
}
};
// 重置配置
const handleReset = () => {
columns.value = [...defaultColumns];
};
</script>
<style lang="less" scoped>
.dynamic-column-config {
max-width: 1200px;
margin: 0 auto;
max-height: 70vh; /* 设置最大高度为视口高度的70% */
overflow-y: auto; /* 超出时显示垂直滚动条 */
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
padding: 12px;
}
:deep(.ant-card-extra) {
padding: 0;
}
}
.column-list {
margin-bottom: 24px;
}
.column-item {
position: relative; /* 为绝对定位的操作区域提供参考 */
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
background: #f6fbff;
}
/* 操作区域样式 */
.action-area {
position: absolute;
top: 8px;
right: 8px;
:deep(.ant-btn) {
width: 24px;
height: 24px;
min-width: 24px;
padding: 0;
.anticon {
font-size: 14px;
}
}
}
}
.column-form {
flex: 1;
width: 100%; /* 确保表单占满宽度 */
}
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-item {
flex: 1;
&.full-width {
flex: 3;
}
}
.form-label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
&::after {
content: '';
}
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
.has-error {
:deep(.ant-input) {
border-color: #ff4d4f;
}
}
.form-actions {
display: none; /* 隐藏原来的删除按钮区域 */
}
.fixed-columns {
margin-top: 24px;
padding-top: 24px;
border-top: 1px dashed #d9d9d9;
.tip-text {
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
}
.fixed-column-item {
margin-bottom: 16px;
padding: 16px;
background: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 6px;
&:last-child {
margin-bottom: 0;
}
:deep(.ant-input-group-addon) {
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
}
.footer-actions {
display: flex;
justify-content: center;
margin-top: 32px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
position: sticky;
bottom: 0;
background: #fff;
padding-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,312 @@
<template>
<BasicDrawer v-bind="getProps">
<div class="dynamic-column-config">
<!-- 列配置列表 -->
<div class="column-list">
<!-- 移除了 draggable 组件直接使用 div 渲染列表 -->
<div v-for="(element, index) in dataSet" :key="index" class="column-item">
<!-- 操作区域 - 放在右上角 -->
<div class="action-area">
<a-button type="text" danger @click="handleRemoveData(index)" title="删除">
<DeleteOutlined />
</a-button>
</div>
<!-- 列配置表单 -->
<div class="column-form">
<div class="form-row">
<div class="form-item" v-for="item in columns" :key="item.name">
<label class="form-label">{{ item.name }}</label>
<!-- 附件输入框 -->
<JImageUpload :maxCount="1" v-if="item.dataType === 'FILE'" v-model:value="element[item.name]"></JImageUpload>
<a-input
v-else
v-model:value="element[item.name]"
:placeholder="'请输入' + item.name"
:class="{ 'has-error': !element[item.name] }"
@blur="validateColumn(element[item.name])"
/>
</div>
</div>
</div>
</div>
</div>
<a-button v-if="!isUpdate" type="dashed" style="width: 100%" @click="handleAddData"> <PlusOutlined />添加数据 </a-button>
</div>
</BasicDrawer>
</template>
<script lang="ts" setup>
import type { DrawerProps } from '/@/components/Drawer';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { computed, unref, ref } from 'vue';
import { useAttrs } from '@/hooks/core/useAttrs';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { saveOrUpdate } from '@/views/super/airag/aiprompts/AiragExtData.api';
import { message } from 'ant-design-vue';
import {JImageUpload} from "@/components/Form";
// Emits声明
const emit = defineEmits(['register', 'success']);
const attrs = useAttrs();
// 消息提示
const { createMessage } = useMessage();
// 注册抽屉
const [registerDrawer, { closeDrawer }] = useDrawerInner(open);
// 抽屉最终props
const getProps = computed(() => {
let drawerProps: Partial<DrawerProps> = {
width: 1000,
title: '数据配置',
showFooter: true,
destroyOnClose: true,
};
let finalProps: Recordable = {
...unref(attrs),
...drawerProps,
okText: '保存',
onOk: handleSubmit,
onCancel: closeDrawer,
onRegister: registerDrawer,
};
return finalProps;
});
/** 更新状态 */
const isUpdate = ref<boolean>(false);
/** dataId */
const dataId = ref<any>('');
/** 数据列表 */
const allData = ref<any>([]);
/** 数据列表 */
const dataSet = ref<any>([]);
// 列配置
const columns = ref<any>([]);
// 验证数据非空
const validateColumn = (value) => {
return value && value.trim() !== '';
};
// 生成唯一ID
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
//添加数据
function handleAddData() {
dataSet.value.push({
id: generateId(),
});
}
//保存数据
async function handleSubmit() {
// 验证数据
const invalidData = dataSet.value.filter((item) => {
return columns.value.some((col) => col.required && !validateColumn(item[col.name]));
});
if (invalidData.length > 0) {
createMessage.error('请填写所有必填项(名称)');
return;
}
try {
// 构造提交数据
let dataSource:any = [];
if (isUpdate.value) {
dataSource = allData.value.map(item =>
item.id === dataSet.value[0].id ? dataSet.value[0] : item
);
} else {
dataSource = allData.value.concat(dataSet.value);
}
const submitData = {
datasetValue: JSON.stringify({
columns: columns.value,
dataSource: dataSource,
}),
id: dataId.value,
};
console.log('提交数据:', submitData);
await saveOrUpdate(submitData, true, false);
message.success('数据保存成功!');
// 关闭弹窗并触发成功事件
closeDrawer();
emit('success');
} catch (error) {
console.error('提交失败:', error);
} finally {
//loading.value = false;
}
}
// 删除列
const handleRemoveData = (index: number) => {
dataSet.value.splice(index, 1);
};
/** 抽屉开启 */
function open(data) {
isUpdate.value = data.isUpdate;
dataId.value = data?.id || '';
allData.value = data?.dataSource || [];
columns.value = data?.columns || [];
console.log("columns.value",columns.value)
if (isUpdate.value) {
console.log("data?.record",data?.record)
dataSet.value = data?.record?[data?.record] : [];
} else {
dataSet.value = [];
}
}
</script>
<style scoped lang="less">
.dynamic-column-config {
max-width: 1200px;
margin: 0 auto;
max-height: calc(100vh - 100px); /* 设置最大高度为视口高度的70% */
overflow-y: auto; /* 超出时显示垂直滚动条 */
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
padding: 12px;
}
:deep(.ant-card-extra) {
padding: 0;
}
}
.column-list {
margin-bottom: 24px;
}
.column-item {
position: relative; /* 为绝对定位的操作区域提供参考 */
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
background: #f6fbff;
}
/* 操作区域样式 */
.action-area {
position: absolute;
top: 8px;
right: 8px;
:deep(.ant-btn) {
width: 24px;
height: 24px;
min-width: 24px;
padding: 0;
.anticon {
font-size: 14px;
}
}
}
}
.column-form {
flex: 1;
width: 100%; /* 确保表单占满宽度 */
}
.form-row {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-item {
flex: 1;
&.full-width {
flex: 3;
}
}
.form-label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
&::after {
content: '';
}
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
.has-error {
:deep(.ant-input) {
border-color: #ff4d4f;
}
}
.form-actions {
display: none; /* 隐藏原来的删除按钮区域 */
}
.fixed-columns {
margin-top: 24px;
padding-top: 24px;
border-top: 1px dashed #d9d9d9;
.tip-text {
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
}
.fixed-column-item {
margin-bottom: 16px;
padding: 16px;
background: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 6px;
&:last-child {
margin-bottom: 0;
}
:deep(.ant-input-group-addon) {
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
}
.footer-actions {
display: flex;
justify-content: center;
margin-top: 32px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
position: sticky;
bottom: 0;
background: #fff;
padding-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<BasicModal v-bind="$attrs" :zIndex="999" @register="registerModal" :canFullscreen="true" :footer="null" defaultFullscreen destroyOnClose title="评测集">
<div class="modal-content">
<div class="top-section">
<div class="header-actions">
<a-button type="primary" v-if="columns && columns.length > 0" @click="handleAddData">
<Icon icon="ant-design:plus-outlined" />
新增数据
</a-button>
<a-button @click="columnConfig" class="config-btn">
<Icon icon="ant-design:setting-outlined" />
列配置
</a-button>
</div>
</div>
<div class="table-container">
<a-table
:columns="columns"
:dataSource="dataSource"
:pagination="pagination"
rowKey="id"
class="data-table"
:scroll="{ x: 'max-content' }"
>
<template #action="{ record }">
<div class="action-buttons">
<a-button size="small" @click="handleEditDataset(record)" class="action-btn edit-btn">
<Icon icon="ant-design:edit-outlined" />
</a-button>
<a-button size="small" @click="handleDelete(record)" class="action-btn delete-btn">
<Icon icon="ant-design:delete-outlined" />
</a-button>
</div>
</template>
</a-table>
</div>
</div>
</BasicModal>
<!--自定义列配置 -->
<AiragDataSetColumnModal @register="registerDataSetColumnModal" @success="reload" />
<!--自定义数据配置 -->
<AiragDataSetDataDrawer @register="registerDataSetDataDrawer" @success="reload" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModal, useModalInner } from '/@/components/Modal';
import AiragDataSetColumnModal from './AiragDataSetColumnModal.vue';
import AiragDataSetDataDrawer from './AiragDataSetDataDrawer.vue';
import {queryById, saveOrUpdate} from '../AiragExtData.api';
import { useDrawer } from '@/components/Drawer';
import { cloneDeep } from 'lodash-es';
import {useMessage} from "@/hooks/web/useMessage";
// Emits声明
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const dataId = ref<string>('');
const columns = ref<any>([]);
const dataSource = ref<any>([]);
const datasetValue = ref<any>({});
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
//表单赋值
const [registerModal] = useModalInner(async (data) => {
dataId.value = data.record?.id;
initData(data.record)
});
//注册列配置
const [registerDataSetColumnModal, { openModal: openColumnModal }] = useModal();
//注册数据抽屉
const [registerDataSetDataDrawer, { openDrawer }] = useDrawer();
/**
* 刷新数据
*/
function reload() {
queryById({ id: dataId.value }).then((res) => {
if (res.success && res.result) {
initData(res.result)
}
});
emit('success');
}
/**
* 初始化数据
*/
function initData(result) {
dataSource.value = []
columns.value = []
datasetValue.value = result?.datasetValue ? JSON.parse(result?.datasetValue) : {};
if (datasetValue.value?.columns) {
datasetValue.value?.columns.forEach((item) => {
columns.value.push({
title: item.name,
dataIndex: item.name,
key: item.name,
fixed: 'center',
});
});
}
dataSource.value = datasetValue.value?.dataSource || [];
//是否已经包含操作列
const hasAction = columns.value.some(item=>item.key == 'action');
if(!hasAction){
columns.value.push({
title: "操作",
dataIndex: 'action',
key: 'action',
fixed: 'right',
width: 120,
slots: { customRender: 'action' },
});
}
}
/**
* 打开列配置
*/
function columnConfig() {
openColumnModal(true, {
id: dataId.value,
datasetValue: cloneDeep(datasetValue.value),
});
}
/**
* 新增数据点击事件
*/
function handleAddData() {
if(!datasetValue.value?.columns || datasetValue.value?.columns.length == 0){
createMessage.warning('请先配置列信息');
return;
}
openDrawer(true, {
id: dataId.value,
dataSource:cloneDeep(datasetValue.value.dataSource),
columns:cloneDeep(datasetValue.value.columns),
isUpdate:false
});
}
/**
* 编辑数据点击事件
* @param record
*/
function handleEditDataset(record) {
openDrawer(true, {
id: dataId.value,
dataSource: cloneDeep(dataSource.value),
columns: cloneDeep(datasetValue.value?.columns),
record: cloneDeep(record),
isUpdate:true
});
}
/**
* 删除数据点击事件
*/
function handleDelete(record) {
//删除数据
dataSource.value = dataSource.value.filter((item) => item.id !== record.id);
refreshDataset()
}
/**
* 表单提交事件
*/
async function refreshDataset() {
//提交表单
const submitData = {
datasetValue: JSON.stringify({
columns: datasetValue.value.columns,
dataSource: dataSource.value,
}),
id: dataId.value,
};
console.log('提交数据:', submitData);
await saveOrUpdate(submitData, true, false);
reload()
}
</script>
<style lang="less" scoped>
.modal-content {
display: flex;
flex-direction: column;
height: 100%;
}
.top-section {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
.config-btn {
color: #1890ff;
border-color: #1890ff;
}
.table-container {
flex: 1;
overflow: auto;
}
.data-table {
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.ant-table-tbody > tr:hover) {
background-color: #f5f5f5;
}
:deep(.ant-table-pagination) {
margin: 16px 0;
}
}
.action-buttons {
display: flex;
gap: 8px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
}
.edit-btn {
color: #1890ff;
border-color: #1890ff;
&:hover {
color: #40a9ff;
border-color: #40a9ff;
}
}
.delete-btn {
color: #ff4d4f;
border-color: #ff4d4f;
&:hover {
color: #ff7875;
border-color: #ff7875;
}
}
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :maxHeight="500" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="AiragExtDataForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from '../AiragExtData.data';
import { saveOrUpdate } from '../AiragExtData.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
baseRowStyle: { padding: '0 20px' },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch (e) {
console.log("e", e)
return Promise.reject(e);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,275 @@
<template>
<BasicDrawer v-bind="getProps">
<BasicTable @register="registerTable">
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" /> </template
></BasicTable>
</BasicDrawer>
<AiragTrackDetailModal @register="registerTrackDetailModal" />
</template>
<script lang="ts" setup>
import type { DrawerProps } from '/@/components/Drawer';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { computed, unref, ref } from 'vue';
import { useAttrs } from '@/hooks/core/useAttrs';
import { BasicTable, TableAction } from '@/components/Table';
import { useModal } from '@/components/Modal';
import { getTrackList, deleteOne } from '@/views/super/airag/aiprompts/AiragExtData.api';
import {useListPage} from "@/hooks/system/useListPage";
import AiragTrackDetailModal from './AiragTrackDetailModal.vue';
// Emits声明
const attrs = useAttrs();
const [registerTrackDetailModal, { openModal }] = useModal();
//列配置
const trackColumns = ref([
{ title: '问题', dataIndex: 'userQuery', key: 'userQuery' },
{ title: '提示词答案', dataIndex: 'promptAnswer', key: 'promptAnswer' },
{ title: '评估分数', dataIndex: 'answerScore', key: 'answerScore' },
{ title: '版本号', dataIndex: 'version', key: 'version' },
]);
// 注册抽屉
const [registerDrawer, { closeDrawer }] = useDrawerInner(open);
// 表格定义
const { tableContext } = useListPage({
designScope: 'agent-config',
tableProps: {
title: '调用记录',
api: getTrackList,
columns: trackColumns.value,
canResize: false,
rowSelection: {
columnWidth: 20,
},
immediate: false,
beforeFetch: async (params) => {
return Object.assign(params, { metadata: dataId.value });
},
afterFetch: async (res) => {
if (res.length > 0) {
res.forEach((item) => {
if (item.dataValue) {
let dataValue = JSON.parse(item.dataValue);
item.answerScore = dataValue.answerScore;
item.userQuery = dataValue.userQuery;
item.promptAnswer = dataValue.promptAnswer;
}
});
}
return res;
},
},
});
const [registerTable, { reload }] = tableContext;
// 抽屉最终props
const getProps = computed(() => {
let drawerProps: Partial<DrawerProps> = {
width: 1000,
title: '调用记录',
destroyOnClose: true,
};
let finalProps: Recordable = {
...unref(attrs),
...drawerProps,
onCancel: closeDrawer,
onRegister: registerDrawer,
};
return finalProps;
});
/** dataId */
const dataId = ref<any>('');
/** 抽屉开启 */
function open(data) {
dataId.value = data.record?.id || '';
reload();
}
/**
* 删除
*/
function handleDelete(record) {
console.log(record, 'record');
deleteOne({ id: record.id},reload)
}
/**
* 详情
* @param record
*/
function handleDetail(record) {
openModal(true, { dataValue: record.dataValue });
}
/*
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
},
];
}
</script>
<style scoped lang="less">
.dynamic-column-config {
max-width: 1200px;
margin: 0 auto;
max-height: calc(100vh - 100px); /* 设置最大高度为视口高度的70% */
overflow-y: auto; /* 超出时显示垂直滚动条 */
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
padding: 12px;
}
:deep(.ant-card-extra) {
padding: 0;
}
}
.column-list {
margin-bottom: 24px;
}
.column-item {
position: relative; /* 为绝对定位的操作区域提供参考 */
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
background: #f6fbff;
}
/* 操作区域样式 */
.action-area {
position: absolute;
top: 8px;
right: 8px;
:deep(.ant-btn) {
width: 24px;
height: 24px;
min-width: 24px;
padding: 0;
.anticon {
font-size: 14px;
}
}
}
}
.column-form {
flex: 1;
width: 100%; /* 确保表单占满宽度 */
}
.form-row {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-item {
flex: 1;
&.full-width {
flex: 3;
}
}
.form-label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
&::after {
content: '';
}
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
.has-error {
:deep(.ant-input) {
border-color: #ff4d4f;
}
}
.form-actions {
display: none; /* 隐藏原来的删除按钮区域 */
}
.fixed-columns {
margin-top: 24px;
padding-top: 24px;
border-top: 1px dashed #d9d9d9;
.tip-text {
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
}
.fixed-column-item {
margin-bottom: 16px;
padding: 16px;
background: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 6px;
&:last-child {
margin-bottom: 0;
}
:deep(.ant-input-group-addon) {
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
}
.footer-actions {
display: flex;
justify-content: center;
margin-top: 32px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
position: sticky;
bottom: 0;
background: #fff;
padding-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :maxHeight="500" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="AiragPromptsForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from '../AiragPrompts.data';
import { saveOrUpdate } from '../AiragPrompts.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
baseRowStyle: { padding: '0 20px' },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//日期个性化选择
const fieldPickers = reactive({});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success',values);
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:canFullscreen="true"
:footer="null"
destroyOnClose
title="详情"
wrapClassName="track-detail-modal"
>
<div class="track-detail-container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<a-button
type="primary"
size="small"
@click="toggleFormat"
class="format-btn"
>
<Icon :icon="isFormatted ? 'ant-design:compress-outlined' : 'ant-design:expand-outlined'" />
{{ isFormatted ? '收起' : '展开' }}
</a-button>
</div>
<!-- JSON内容显示区域 -->
<div class="json-content-wrapper">
<div v-if="!trackContent" class="empty-state">
<Icon icon="ant-design:inbox-outlined" style="font-size: 48px; color: #d9d9d9;" />
<p>暂无轨迹数据</p>
</div>
<div v-else class="json-display">
<pre v-if="isFormatted" >{{ formattedJson }}</pre>
<pre v-else class="raw-json">{{ trackContent }}</pre>
</div>
</div>
<!-- 底部信息栏 -->
<div v-if="trackContent" class="info-bar">
<span class="info-item">
<Icon icon="ant-design:info-circle-outlined" />
{{ isFormatted ? '已格式化' : '原始数据' }}
</span>
<!-- <span class="info-item">-->
<!-- <Icon icon="ant-design:file-text-outlined" />-->
<!-- 字符数: {{ trackContent.length }}-->
<!-- </span>-->
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import Icon from '/@/components/Icon/src/Icon.vue';
// 轨迹详情
const trackContent = ref('');
const isFormatted = ref(true);
// 计算属性格式化JSON
const formattedJson = computed(() => {
try {
// 尝试解析为JSON并格式化
const jsonData = JSON.parse(trackContent.value);
return JSON.stringify(jsonData, null, 2);
} catch (e) {
// 如果不是有效的JSON返回原始内容
return trackContent.value;
}
});
// 切换格式化状态
const toggleFormat = () => {
isFormatted.value = !isFormatted.value;
};
// 复制JSON内容
const copyJson = async () => {
try {
const content = isFormatted.value ? formattedJson.value : trackContent.value;
await navigator.clipboard.writeText(content);
// 这里可以添加一个轻提示
console.log('JSON内容已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
}
};
// 表单赋值
const [registerModal] = useModalInner(async (data) => {
trackContent.value = data?.dataValue || '';
// 默认显示格式化内容
isFormatted.value = true;
});
</script>
<style lang="less" scoped>
@primary-color: #1890ff;
@border-color: #e8e8e8;
@background-color: #f5f7fa;
@text-color: #262626;
@text-secondary: #8c8c8c;
.track-detail-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 400px;
}
// 顶部工具栏
.toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
padding: 12px 16px;
background: @background-color;
border-radius: 8px;
border: 1px solid @border-color;
.format-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.3s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.2);
}
}
}
// JSON内容显示区域
.json-content-wrapper {
flex: 1;
overflow: auto;
background: white;
border: 1px solid @border-color;
border-radius: 8px;
padding: 16px;
position: relative;
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: @text-secondary;
p {
margin-top: 16px;
font-size: 16px;
}
}
.json-display {
pre {
margin: 0;
padding: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
max-height: 500px;
overflow: auto;
}
.formatted-json {
color: @text-color;
// JSON语法高亮样式
&::before {
content: '{';
color: #d73a49;
}
&::after {
content: '}';
color: #d73a49;
}
}
.raw-json {
color: @text-secondary;
background: #fafafa;
padding: 12px;
border-radius: 4px;
border-left: 4px solid @primary-color;
}
}
}
// 底部信息栏
.info-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding: 12px 16px;
background: @background-color;
border-radius: 8px;
border: 1px solid @border-color;
font-size: 14px;
color: @text-secondary;
.info-item {
display: flex;
align-items: center;
gap: 6px;
svg {
font-size: 16px;
}
}
}
// 滚动条样式
.json-content-wrapper::-webkit-scrollbar,
.json-display pre::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.json-content-wrapper::-webkit-scrollbar-track,
.json-display pre::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.json-content-wrapper::-webkit-scrollbar-thumb,
.json-display pre::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
// 响应式设计
@media (max-width: 768px) {
.toolbar {
padding: 10px 12px;
}
.json-content-wrapper {
padding: 12px;
.json-display pre {
font-size: 12px;
}
}
.info-bar {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
}
</style>
<style lang="less">
// 模态框样式调整
.track-detail-modal {
.ant-modal-body {
padding: 24px;
min-height: 500px;
}
}
</style>

View File

@@ -0,0 +1,458 @@
<template>
<div class="ai-assessment-container">
<!-- 标题和操作栏 -->
<div class="assessment-header">
<div class="header-left">
<h5 class="title">AI内容评估</h5>
</div>
<div class="header-right" v-if="variable && variable.length > 0">
<a-button type="primary" ghost class="clear-btn" @click="handleClear">
<template #icon><DeleteOutlined /></template>
清空
</a-button>
<a-button type="primary" :loading="loading" class="run-btn" @click="handleRun">
<template #icon><PlayCircleOutlined /></template>
运行
</a-button>
</div>
</div>
<!-- 评估内容区域 -->
<div class="assessment-content" v-if="variable && variable.length > 0">
<div class="input-section" v-for="item in variable" :key="item.name">
<div class="section-header output-header">
<span class="section-title">{{item.name}}</span>
</div>
<div class="section-content output-content">
<a-input v-model:value="item.value" placeholder="请输入" />
</div>
</div>
</div>
<div v-else class="empty-container">
<Empty
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无变量"
class="custom-empty"
/>
</div>
<!-- 结果内容区域 -->
<div class="debug-result-container" v-if="result">
<!-- 调试成功提示 -->
<div class="debug-success">
<div class="success-header">
<div class="success-icon"></div>
<span class="success-text">调试成功</span>
</div>
<!-- 分数显示 -->
<!-- <div class="score-section">-->
<!-- <span class="score-label">0 </span>-->
<!-- <div class="score-note">-->
<!-- <span class="note-text">得分仅预览效果非实际结果</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- 原因分析 -->
<div class="reason-section">
<!-- <div class="reason-label">原因:</div>-->
<div class="reason-content">
<span>{{result}}</span>
</div>
</div>
<!-- 免责声明 -->
<div class="disclaimer">
<span class="disclaimer-text">内容由AI生成无法确保真实准确仅供参考</span>
</div>
</div>
<!-- 调试失败示例可选显示 -->
<div v-if="showFailureExample" class="debug-failure">
<div class="failure-header">
<div class="failure-icon"></div>
<span class="failure-text">调试失败</span>
</div>
<div class="error-message">
<span>代码中存在语法错误请检查后重新调试</span>
</div>
</div>
</div>
<!-- 底部说明 -->
<!-- <div class="assessment-footer">-->
<!-- <div class="footer-content">-->
<!-- <InfoCircleOutlined class="footer-icon" />-->
<!-- <span>内容由AI生成无法确保真实准确仅供参考</span>-->
<!-- </div>-->
<!-- </div>-->
</div>
</template>
<script setup>
import { ref, onMounted,watch } from 'vue'
import { Empty } from 'ant-design-vue';
import {
DeleteOutlined,
PlayCircleOutlined,
} from '@ant-design/icons-vue'
// 组件属性
const props = defineProps({
// 原始内容
content: {
type: String,
default: ``
},
})
// 控制是否显示失败示例
const showFailureExample = ref(false)
// 运行状态
const loading = ref(false)
// 响应式数据
const variable = ref([])
// 结果
const result = ref("")
// 从内容中提取参数
const extractContent = () => {
if(props.content){
const vars = props.content.match(/{{\s*([^}\s]+)\s*}}/g);
if(vars && vars.length > 0){
variable.value = vars.map((match) => ({value:"",name:match.replace(/{{\s*|\s*}}/g, '')}))
}
}else{
variable.value = []
}
}
// 清空操作
const handleClear = () => {
variable.value.forEach((item) => item.value = '')
}
// 运行操作
const handleRun = () => {
// 触发运行事件
emit('run', variable.value)
}
// 定义事件
const emit = defineEmits(['clear', 'run'])
// 生命周期
onMounted(() => {
extractContent()
})
// 监听内容变化
watch(() => props.content, extractContent)
defineExpose({
loading,
result
})
</script>
<style scoped>
.ai-assessment-container {
width: 100%;
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
overflow: hidden;
}
/* 头部样式 */
.assessment-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
border-radius: 8px 8px 0 0;
}
.header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #262626;
}
.header-right {
display: flex;
gap: 12px;
}
.clear-btn {
color: #ff4d4f;
border-color: #ff4d4f;
}
.clear-btn:hover {
color: #ff7875;
border-color: #ff7875;
}
.run-btn {
background: #1890ff;
border-color: #1890ff;
}
.run-btn:hover {
background: #40a9ff;
border-color: #40a9ff;
}
/* 内容区域样式 */
.assessment-content {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* 通用区块样式 */
.input-section,
.output-section {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.section-title {
font-size: 14px;
font-weight: 500;
}
/* 输出区域 */
.output-header {
background: #e6f7ff;
border-bottom: 1px solid #91d5ff;
}
.output-icon {
color: #1890ff;
}
.output-content {
padding: 16px;
background: #e6f7ff;
color: #096dd9;
font-size: 14px;
line-height: 1.5;
min-height: 80px;
}
/* 底部样式 */
.assessment-footer {
padding: 12px 20px;
background: #fff;
border-top: 1px solid #e8e8e8;
border-radius: 0 0 8px 8px;
}
.footer-content {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #8c8c8c;
}
.footer-icon {
font-size: 12px;
}
.debug-result-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
max-width: 600px;
margin: 20px auto;
}
/* 成功样式 */
.debug-success {
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
background-color: #f6f8fa;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.success-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.success-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #2ea44f;
color: white;
font-weight: bold;
font-size: 14px;
margin-right: 8px;
}
.success-text {
font-size: 16px;
font-weight: 600;
color: #24292e;
}
/* 分数部分 */
.score-section {
margin-bottom: 16px;
}
.score-label {
display: inline-block;
font-size: 14px;
color: #24292e;
margin-bottom: 4px;
}
.score-note {
font-size: 12px;
color: #57606a;
}
.note-text {
display: inline-block;
padding: 4px 8px;
background-color: #f0f0f0;
border-radius: 4px;
}
/* 原因部分 */
.reason-section {
margin-bottom: 16px;
}
.reason-label {
font-size: 14px;
font-weight: 600;
color: #24292e;
margin-bottom: 4px;
}
.reason-content {
font-size: 14px;
color: #24292e;
padding: 8px 12px;
background-color: white;
border: 1px solid #d0d7de;
border-radius: 4px;
}
/* 免责声明 */
.disclaimer {
padding: 8px;
background-color: #fff8e6;
border: 1px solid #ffd33d;
border-radius: 4px;
}
.disclaimer-text {
font-size: 12px;
color: #57606a;
font-style: italic;
}
/* 失败样式 */
.debug-failure {
margin-top: 20px;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
background-color: #fcf2f2;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.failure-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.failure-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #cf222e;
color: white;
font-weight: bold;
font-size: 14px;
margin-right: 8px;
}
.failure-text {
font-size: 16px;
font-weight: 600;
color: #cf222e;
}
.error-message {
font-size: 14px;
color: #24292e;
padding: 8px 12px;
background-color: white;
border: 1px solid #d0d7de;
border-radius: 4px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.assessment-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.header-right {
justify-content: flex-end;
}
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
padding: 40px 0;
}
.custom-empty {
:deep(.ant-empty-image) {
height: 80px;
margin-bottom: 16px;
}
:deep(.ant-empty-description) {
font-size: 14px;
color: #8c8c8c;
}
}
</style>