refactor(ui): extract temporary variables panel
Reuse a shared temp-variable editor in test panels and show it above image test variants for parity with Pro.
This commit is contained in:
1 parent
84b1751cc7
commit
0f526630a2
6 files changed
+450
-714
No files matched your search
@@ -1,159 +1,12 @@
|
||||
<template>
|
||||
<NFlex vertical :style="{ height: mode === 'full' ? '100%' : 'auto', gap: '12px' }">
|
||||
<!-- 变量值输入表单 -->
|
||||
<NCard
|
||||
:title="t('test.variables.formTitle')"
|
||||
size="small"
|
||||
:bordered="true"
|
||||
:style="{ flexShrink: 0 }"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NSpace :size="8">
|
||||
<NTag :bordered="false" type="info" size="small">
|
||||
{{ t("test.variables.tempCount", { count: displayVariables.length }) }}
|
||||
</NTag>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
:loading="isGenerating"
|
||||
:disabled="displayVariables.length === 0 || isGenerating"
|
||||
@click="handleGenerateValues"
|
||||
>
|
||||
{{ isGenerating
|
||||
? t('test.variableValueGeneration.generating')
|
||||
: t('test.variableValueGeneration.generateButton')
|
||||
}}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleClearAllVariables"
|
||||
>
|
||||
{{ t("test.variables.clearAll") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
@click="emit('open-global-variables')"
|
||||
>
|
||||
{{ t("contextMode.actions.globalVariables") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<NSpace vertical :size="12">
|
||||
<!-- 变量输入项 -->
|
||||
<div
|
||||
v-for="varName in displayVariables"
|
||||
:key="varName"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}"
|
||||
>
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:type="
|
||||
getVariableSource(varName) === 'predefined'
|
||||
? 'success'
|
||||
: getVariableSource(varName) === 'test'
|
||||
? 'warning'
|
||||
: 'default'
|
||||
"
|
||||
:style="{ minWidth: '120px', flexShrink: 0 }"
|
||||
>
|
||||
<span v-text="`{{${varName}}}`"></span>
|
||||
</NTag>
|
||||
<NInput
|
||||
:value="getVariableDisplayValue(varName)"
|
||||
:placeholder="getVariablePlaceholder(varName)"
|
||||
size="small"
|
||||
:style="{ flex: 1 }"
|
||||
@update:value="
|
||||
handleVariableValueChange(varName, $event)
|
||||
"
|
||||
/>
|
||||
<!-- 删除按钮 (仅临时变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleDeleteVariable(varName)"
|
||||
:title="t('test.variables.delete')"
|
||||
>
|
||||
🗑️
|
||||
</NButton>
|
||||
<!-- 保存到全局按钮 (仅测试变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleSaveToGlobal(varName)"
|
||||
:title="t('test.variables.saveToGlobal')"
|
||||
>
|
||||
💾
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- 无变量提示 -->
|
||||
<NEmpty
|
||||
v-if="displayVariables.length === 0"
|
||||
:description="t('test.variables.noVariables')"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<NSpace :size="8" justify="end">
|
||||
<!-- 添加变量按钮 -->
|
||||
<NButton
|
||||
size="small"
|
||||
@click="showAddVariableDialog = true"
|
||||
>
|
||||
{{ t("test.variables.addVariable") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 添加变量对话框 -->
|
||||
<NModal
|
||||
v-model:show="showAddVariableDialog"
|
||||
preset="dialog"
|
||||
:title="t('test.variables.addVariable')"
|
||||
:positive-text="t('common.confirm')"
|
||||
:negative-text="t('common.cancel')"
|
||||
:on-positive-click="handleAddVariable"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<NSpace vertical :size="12" style="margin-top: 16px;">
|
||||
<NFormItem
|
||||
:label="t('variableExtraction.variableName')"
|
||||
:validation-status="
|
||||
newVariableNameError ? 'error' : undefined
|
||||
"
|
||||
:feedback="newVariableNameError"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="newVariableName"
|
||||
:placeholder="
|
||||
t('variableExtraction.variableNamePlaceholder')
|
||||
"
|
||||
@input="validateNewVariableName"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('variableExtraction.variableValue')">
|
||||
<NInput
|
||||
v-model:value="newVariableValue"
|
||||
:placeholder="
|
||||
t('variableExtraction.variableValuePlaceholder')
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
<TemporaryVariablesPanel
|
||||
:manager="variableManager"
|
||||
:show-generate-values="true"
|
||||
:is-generating="isGenerating"
|
||||
@generate-values="handleGenerateValues"
|
||||
@open-global-variables="emit('open-global-variables')"
|
||||
/>
|
||||
|
||||
<template v-if="mode === 'full'">
|
||||
<!-- 控制工具栏 -->
|
||||
@@ -244,13 +97,6 @@ import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
NFlex,
|
||||
NCard,
|
||||
NButton,
|
||||
NTag,
|
||||
NSpace,
|
||||
NInput,
|
||||
NEmpty,
|
||||
NModal,
|
||||
NFormItem,
|
||||
} from "naive-ui";
|
||||
import { useResponsive } from '../../composables/ui/useResponsive';
|
||||
import { usePerformanceMonitor } from "../../composables/performance/usePerformanceMonitor";
|
||||
@@ -260,6 +106,7 @@ import { useVariableValueGeneration } from "../../composables/variable/useVariab
|
||||
import { useToast } from "../../composables/ui/useToast";
|
||||
import TestControlBar from "../TestControlBar.vue";
|
||||
import TestResultSection from "../TestResultSection.vue";
|
||||
import TemporaryVariablesPanel from "../variable/TemporaryVariablesPanel.vue";
|
||||
import VariableValuePreviewDialog from "../variable/VariableValuePreviewDialog.vue";
|
||||
import type { EvaluationResponse, EvaluationType, VariableToGenerate } from '@prompt-optimizer/core';
|
||||
import type { ScoreLevel } from '../../composables/prompt/useEvaluation';
|
||||
@@ -455,20 +302,10 @@ const variableManager = useTestVariableManager({
|
||||
});
|
||||
|
||||
const {
|
||||
showAddVariableDialog,
|
||||
newVariableName,
|
||||
newVariableValue,
|
||||
newVariableNameError,
|
||||
sortedVariables: displayVariables,
|
||||
getVariableSource,
|
||||
getVariableDisplayValue,
|
||||
getVariablePlaceholder,
|
||||
validateNewVariableName,
|
||||
handleVariableValueChange,
|
||||
handleAddVariable,
|
||||
handleDeleteVariable,
|
||||
handleClearAllVariables,
|
||||
handleSaveToGlobal,
|
||||
getVariableValues,
|
||||
setVariableValues,
|
||||
} = variableManager;
|
||||
|
||||
@@ -1,147 +1,9 @@
|
||||
<template>
|
||||
<NFlex vertical :style="{ height: mode === 'full' ? '100%' : 'auto', gap: '12px' }">
|
||||
<!-- 变量值输入表单 -->
|
||||
<NCard
|
||||
:title="t('test.variables.formTitle')"
|
||||
size="small"
|
||||
:bordered="true"
|
||||
:style="{ flexShrink: 0 }"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NSpace :size="8">
|
||||
<NTag :bordered="false" type="info" size="small">
|
||||
{{ t("test.variables.tempCount", { count: displayVariables.length }) }}
|
||||
</NTag>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleClearAllVariables"
|
||||
>
|
||||
{{ t("test.variables.clearAll") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
@click="emit('open-global-variables')"
|
||||
>
|
||||
{{ t("contextMode.actions.globalVariables") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<NSpace vertical :size="12">
|
||||
<!-- 变量输入项 -->
|
||||
<div
|
||||
v-for="varName in displayVariables"
|
||||
:key="varName"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}"
|
||||
>
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:type="
|
||||
getVariableSource(varName) === 'predefined'
|
||||
? 'success'
|
||||
: getVariableSource(varName) === 'test'
|
||||
? 'warning'
|
||||
: 'default'
|
||||
"
|
||||
:style="{ minWidth: '120px', flexShrink: 0 }"
|
||||
>
|
||||
<span v-text="`{{${varName}}}`"></span>
|
||||
</NTag>
|
||||
<NInput
|
||||
:value="getVariableDisplayValue(varName)"
|
||||
:placeholder="getVariablePlaceholder(varName)"
|
||||
size="small"
|
||||
:style="{ flex: 1 }"
|
||||
@update:value="
|
||||
handleVariableValueChange(varName, $event)
|
||||
"
|
||||
/>
|
||||
<!-- 删除按钮 (仅临时变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleDeleteVariable(varName)"
|
||||
:title="t('test.variables.delete')"
|
||||
>
|
||||
🗑️
|
||||
</NButton>
|
||||
<!-- 保存到全局按钮 (仅测试变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
@click="handleSaveToGlobal(varName)"
|
||||
:title="t('test.variables.saveToGlobal')"
|
||||
>
|
||||
💾
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- 无变量提示 -->
|
||||
<NEmpty
|
||||
v-if="displayVariables.length === 0"
|
||||
:description="t('test.variables.noVariables')"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<NSpace :size="8" justify="end">
|
||||
<!-- 添加变量按钮 -->
|
||||
<NButton
|
||||
size="small"
|
||||
@click="showAddVariableDialog = true"
|
||||
>
|
||||
{{ t("test.variables.addVariable") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 添加变量对话框 -->
|
||||
<NModal
|
||||
v-model:show="showAddVariableDialog"
|
||||
preset="dialog"
|
||||
:title="t('test.variables.addVariable')"
|
||||
:positive-text="t('common.confirm')"
|
||||
:negative-text="t('common.cancel')"
|
||||
:on-positive-click="handleAddVariable"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<NSpace vertical :size="12" style="margin-top: 16px;">
|
||||
<NFormItem
|
||||
:label="t('variableExtraction.variableName')"
|
||||
:validation-status="
|
||||
newVariableNameError ? 'error' : undefined
|
||||
"
|
||||
:feedback="newVariableNameError"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="newVariableName"
|
||||
:placeholder="
|
||||
t('variableExtraction.variableNamePlaceholder')
|
||||
"
|
||||
@input="validateNewVariableName"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('variableExtraction.variableValue')">
|
||||
<NInput
|
||||
v-model:value="newVariableValue"
|
||||
:placeholder="
|
||||
t('variableExtraction.variableValuePlaceholder')
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
<TemporaryVariablesPanel
|
||||
:manager="variableManager"
|
||||
@open-global-variables="emit('open-global-variables')"
|
||||
/>
|
||||
|
||||
<template v-if="mode === 'full'">
|
||||
<!-- 控制工具栏 -->
|
||||
@@ -264,20 +126,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||
import { computed, ref, onUnmounted, toRef } from 'vue'
|
||||
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
useMessage,
|
||||
NFlex,
|
||||
NCard,
|
||||
NButton,
|
||||
NTag,
|
||||
NSpace,
|
||||
NInput,
|
||||
NEmpty,
|
||||
NModal,
|
||||
NFormItem,
|
||||
} from "naive-ui";
|
||||
import type {
|
||||
OptimizationMode,
|
||||
@@ -293,9 +147,10 @@ import { useDebounceThrottle } from "../../composables/performance/useDebounceTh
|
||||
import TestControlBar from "../TestControlBar.vue";
|
||||
import TestResultSection from "../TestResultSection.vue";
|
||||
import ToolCallDisplay from "../ToolCallDisplay.vue";
|
||||
import TemporaryVariablesPanel from "../variable/TemporaryVariablesPanel.vue";
|
||||
import { useTestVariableManager } from "../../composables/variable/useTestVariableManager";
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
|
||||
// 性能监控
|
||||
const { recordUpdate, getPerformanceReport } = usePerformanceMonitor("ConversationTestPanel");
|
||||
@@ -494,216 +349,35 @@ const handleTest = throttle(
|
||||
|
||||
// ========== 变量管理 ==========
|
||||
|
||||
// 添加变量对话框状态
|
||||
const showAddVariableDialog = ref(false);
|
||||
const newVariableName = ref("");
|
||||
const newVariableValue = ref("");
|
||||
const newVariableNameError = ref("");
|
||||
const variableManager = useTestVariableManager({
|
||||
globalVariables: toRef(props, 'globalVariables'),
|
||||
predefinedVariables: toRef(props, 'predefinedVariables'),
|
||||
temporaryVariables: toRef(props, 'temporaryVariables'),
|
||||
onVariableChange: (name, value) => {
|
||||
emit('variable-change', name, value)
|
||||
recordUpdate()
|
||||
},
|
||||
onSaveToGlobal: (name, value) => {
|
||||
emit('save-to-global', name, value)
|
||||
recordUpdate()
|
||||
},
|
||||
onVariableRemove: (name) => {
|
||||
emit('temporary-variable-remove', name)
|
||||
recordUpdate()
|
||||
},
|
||||
onVariablesClear: () => {
|
||||
emit('temporary-variables-clear')
|
||||
recordUpdate()
|
||||
},
|
||||
})
|
||||
|
||||
// 测试区临时变量
|
||||
interface TestVariable {
|
||||
value: string;
|
||||
timestamp: number;
|
||||
const getVariableValues = () => {
|
||||
return variableManager.getVariableValues()
|
||||
}
|
||||
|
||||
const testVariables = ref<Record<string, TestVariable>>({});
|
||||
|
||||
// 监听 props.temporaryVariables 变化
|
||||
watch(
|
||||
() => props.temporaryVariables,
|
||||
(newVars) => {
|
||||
const newVarNames = new Set(Object.keys(newVars));
|
||||
for (const name of Object.keys(testVariables.value)) {
|
||||
if (!newVarNames.has(name)) {
|
||||
delete testVariables.value[name];
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(newVars)) {
|
||||
if (!testVariables.value[name]) {
|
||||
testVariables.value[name] = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
} else {
|
||||
testVariables.value[name].value = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 三层变量合并
|
||||
const mergedVariables = computed(() => {
|
||||
const testVarsFlat: Record<string, string> = {};
|
||||
for (const [name, data] of Object.entries(testVariables.value)) {
|
||||
testVarsFlat[name] = data.value;
|
||||
}
|
||||
|
||||
return {
|
||||
...props.globalVariables,
|
||||
...testVarsFlat,
|
||||
...props.predefinedVariables,
|
||||
};
|
||||
});
|
||||
|
||||
// 按时间排序的临时变量列表
|
||||
const sortedTestVariables = computed(() => {
|
||||
const entries = Object.entries(testVariables.value);
|
||||
return entries
|
||||
.sort((a, b) => b[1].timestamp - a[1].timestamp)
|
||||
.map(([name]) => name);
|
||||
});
|
||||
|
||||
// 实际显示的变量列表
|
||||
const displayVariables = computed(() => {
|
||||
return sortedTestVariables.value;
|
||||
});
|
||||
|
||||
// 获取变量的显示值
|
||||
const getVariableDisplayValue = (varName: string): string => {
|
||||
return mergedVariables.value[varName] || "";
|
||||
};
|
||||
|
||||
// 获取变量的占位符提示
|
||||
const getVariablePlaceholder = (varName: string): string => {
|
||||
if (props.predefinedVariables?.[varName]) {
|
||||
return (
|
||||
t("test.variables.inputPlaceholder") +
|
||||
` (${t("variables.source.predefined")})`
|
||||
);
|
||||
}
|
||||
if (props.globalVariables?.[varName]) {
|
||||
return (
|
||||
t("test.variables.inputPlaceholder") +
|
||||
` (${t("variables.source.global")})`
|
||||
);
|
||||
}
|
||||
return t("test.variables.inputPlaceholder");
|
||||
};
|
||||
|
||||
// 事件处理函数
|
||||
const handleVariableValueChange = (varName: string, value: string) => {
|
||||
if (testVariables.value[varName]) {
|
||||
testVariables.value[varName].value = value;
|
||||
} else {
|
||||
testVariables.value[varName] = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
emit("variable-change", varName, value);
|
||||
recordUpdate();
|
||||
};
|
||||
|
||||
const handleClearAllVariables = () => {
|
||||
testVariables.value = {};
|
||||
emit("temporary-variables-clear");
|
||||
message.success(t("test.variables.clearSuccess"));
|
||||
recordUpdate();
|
||||
};
|
||||
|
||||
// 保存测试变量到全局
|
||||
const handleSaveToGlobal = (varName: string) => {
|
||||
const varData = testVariables.value[varName];
|
||||
if (!varData || !varData.value.trim()) {
|
||||
message.warning(t("test.variables.emptyValueWarning"));
|
||||
return;
|
||||
}
|
||||
|
||||
emit("save-to-global", varName, varData.value);
|
||||
message.success(t("test.variables.savedToGlobal"));
|
||||
recordUpdate();
|
||||
};
|
||||
|
||||
// 验证新变量名
|
||||
const validateNewVariableName = () => {
|
||||
const name = newVariableName.value.trim();
|
||||
|
||||
if (!name) {
|
||||
newVariableNameError.value = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^\d/.test(name)) {
|
||||
newVariableNameError.value = t(
|
||||
"variableExtraction.validation.noNumberStart"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[\u4e00-\u9fa5a-zA-Z_][\u4e00-\u9fa5a-zA-Z0-9_]*$/.test(name)) {
|
||||
newVariableNameError.value = t(
|
||||
"variableExtraction.validation.invalidCharacters"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (testVariables.value[name]) {
|
||||
newVariableNameError.value = t(
|
||||
"variableExtraction.validation.duplicateVariable"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
newVariableNameError.value = "";
|
||||
return true;
|
||||
};
|
||||
|
||||
// 添加新变量
|
||||
const handleAddVariable = () => {
|
||||
if (!validateNewVariableName()) {
|
||||
if (!newVariableName.value.trim()) {
|
||||
message.warning(t("test.variables.nameRequired"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = newVariableName.value.trim();
|
||||
handleVariableValueChange(name, newVariableValue.value);
|
||||
if (testVariables.value[name]) {
|
||||
testVariables.value[name].timestamp = Date.now();
|
||||
}
|
||||
message.success(t("test.variables.addSuccess"));
|
||||
|
||||
newVariableName.value = "";
|
||||
newVariableValue.value = "";
|
||||
newVariableNameError.value = "";
|
||||
showAddVariableDialog.value = false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 删除变量
|
||||
const handleDeleteVariable = (varName: string) => {
|
||||
delete testVariables.value[varName];
|
||||
emit("temporary-variable-remove", varName);
|
||||
emit("variable-change", varName, "");
|
||||
message.success(
|
||||
t("test.variables.deleteSuccess", { name: varName })
|
||||
);
|
||||
recordUpdate();
|
||||
};
|
||||
|
||||
// 暴露变量值供外部访问
|
||||
const getVariableValues = () => {
|
||||
return { ...mergedVariables.value };
|
||||
};
|
||||
|
||||
// 设置变量值
|
||||
const setVariableValues = (values: Record<string, string>) => {
|
||||
for (const [name, value] of Object.entries(values)) {
|
||||
emit("variable-change", name, value);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取变量来源
|
||||
const getVariableSource = (varName: string): "predefined" | "test" | "global" | "empty" => {
|
||||
if (props.predefinedVariables?.[varName]) return "predefined";
|
||||
if (testVariables.value[varName]) return "test";
|
||||
if (props.globalVariables?.[varName]) return "global";
|
||||
return "empty";
|
||||
};
|
||||
variableManager.setVariableValues(values)
|
||||
}
|
||||
|
||||
// 开发环境下的性能调试
|
||||
if (import.meta.env.DEV) {
|
||||
|
||||
@@ -402,6 +402,11 @@
|
||||
<!-- 右侧:图像生成测试区域(图像模型,多列 variants) -->
|
||||
<div ref="testPaneRef" class="split-pane" style="min-width: 0; height: 100%; overflow: hidden;">
|
||||
<NFlex vertical :style="{ height: '100%', gap: '12px' }">
|
||||
<TemporaryVariablesPanel
|
||||
:manager="temporaryVariablePanelManager"
|
||||
:disabled="isOptimizing"
|
||||
:show-open-global-variables="false"
|
||||
/>
|
||||
<!-- 顶部:列数与全局操作 -->
|
||||
<NCard size="small" :style="{ flexShrink: 0 }">
|
||||
<div class="test-area-top">
|
||||
@@ -742,10 +747,16 @@ import type { SelectOption } from "../../types/select-options";
|
||||
import { useToast } from "../../composables/ui/useToast";
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { VariableAwareInput } from '../variable-extraction'
|
||||
import TemporaryVariablesPanel from '../variable/TemporaryVariablesPanel.vue'
|
||||
import { useTemporaryVariables } from '../../composables/variable/useTemporaryVariables'
|
||||
import { useVariableAwareInputBridge } from '../../composables/variable/useVariableAwareInputBridge'
|
||||
import { useTestVariableManager } from '../../composables/variable/useTestVariableManager'
|
||||
import type { VariableManagerHooks } from '../../composables/prompt/useVariableManager'
|
||||
import { platform } from '../../utils/platform'
|
||||
import { findMissingVariables, replaceVariablesInContent } from '../../utils/prompt-variables'
|
||||
import {
|
||||
buildPromptExecutionContext,
|
||||
hashString,
|
||||
hashVariables,
|
||||
} from '../../utils/prompt-variables'
|
||||
import {
|
||||
useImageImage2ImageSession,
|
||||
type TestColumnCount,
|
||||
@@ -788,75 +799,48 @@ const services = inject<Ref<AppServices | null>>("services", ref(null));
|
||||
const variableManager = inject<VariableManagerHooks | null>('variableManager', null)
|
||||
const tempVarsManager = useTemporaryVariables()
|
||||
|
||||
// VariableAwareInput 需要的变量数据
|
||||
const purePredefinedVariables = computed(() => {
|
||||
const all = variableManager?.allVariables.value || {}
|
||||
const custom = variableManager?.customVariables.value || {}
|
||||
const predefined: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(all)) {
|
||||
if (!(key in custom)) predefined[key] = value
|
||||
}
|
||||
return predefined
|
||||
const {
|
||||
variableInputData,
|
||||
predefinedVariableValues: purePredefinedVariables,
|
||||
handleVariableExtracted,
|
||||
handleAddMissingVariable,
|
||||
} = useVariableAwareInputBridge({
|
||||
enabled: computed(() => true),
|
||||
isReady: computed(() => variableManager?.isReady.value ?? false),
|
||||
globalVariables: computed(() => variableManager?.customVariables.value || {}),
|
||||
temporaryVariables: tempVarsManager.temporaryVariables,
|
||||
allVariables: computed(() => variableManager?.allVariables.value || {}),
|
||||
saveGlobalVariable: (name, value) => variableManager?.addVariable(name, value),
|
||||
saveTemporaryVariable: (name, value) => tempVarsManager.setVariable(name, value),
|
||||
logPrefix: 'ImageImage2ImageWorkspace',
|
||||
})
|
||||
|
||||
const variableInputData = computed(() => {
|
||||
if (!variableManager?.isReady.value) return null
|
||||
|
||||
return {
|
||||
existingGlobalVariables: Object.keys(variableManager.customVariables.value || {}),
|
||||
existingTemporaryVariables: Object.keys(tempVarsManager.temporaryVariables.value || {}),
|
||||
predefinedVariables: Object.keys(purePredefinedVariables.value || {}),
|
||||
globalVariableValues: variableManager.customVariables.value || {},
|
||||
temporaryVariableValues: tempVarsManager.temporaryVariables.value || {},
|
||||
predefinedVariableValues: purePredefinedVariables.value || {},
|
||||
}
|
||||
const temporaryVariablePanelManager = useTestVariableManager({
|
||||
globalVariables: computed(() => variableManager?.customVariables.value || {}),
|
||||
predefinedVariables: purePredefinedVariables,
|
||||
temporaryVariables: computed(() => tempVarsManager.temporaryVariables.value),
|
||||
onVariableChange: (name, value) => {
|
||||
tempVarsManager.setVariable(name, value)
|
||||
},
|
||||
onVariableRemove: (name) => {
|
||||
tempVarsManager.deleteVariable(name)
|
||||
},
|
||||
onVariablesClear: () => {
|
||||
tempVarsManager.clearAll()
|
||||
},
|
||||
onSaveToGlobal: (name, value) => {
|
||||
if (!variableManager || !variableManager.isReady.value) {
|
||||
throw new Error('variable manager not ready')
|
||||
}
|
||||
variableManager.addVariable(name, value)
|
||||
},
|
||||
})
|
||||
|
||||
const handleOriginalPromptInput = (value: string) => {
|
||||
originalPrompt.value = value
|
||||
}
|
||||
|
||||
const handleVariableExtracted = (data: {
|
||||
variableName: string
|
||||
variableValue: string
|
||||
variableType: 'global' | 'temporary'
|
||||
}) => {
|
||||
if (data.variableType === 'global') {
|
||||
try {
|
||||
variableManager?.addVariable(data.variableName, data.variableValue)
|
||||
toast.success(t('variableExtraction.savedToGlobal', { name: data.variableName }))
|
||||
} catch (error) {
|
||||
console.error('[ImageImage2ImageWorkspace] Failed to save global variable:', error)
|
||||
toast.error(
|
||||
t('variableExtraction.saveFailedWithUndo', {
|
||||
name: data.variableName,
|
||||
undo: platform.getUndoKey(),
|
||||
}),
|
||||
{ duration: 8000, closable: true },
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
tempVarsManager.setVariable(data.variableName, data.variableValue)
|
||||
toast.success(t('variableExtraction.savedToTemporary', { name: data.variableName }))
|
||||
} catch (error) {
|
||||
console.error('[ImageImage2ImageWorkspace] Failed to save temporary variable:', error)
|
||||
toast.error(
|
||||
t('variableExtraction.saveFailedWithUndo', {
|
||||
name: data.variableName,
|
||||
undo: platform.getUndoKey(),
|
||||
}),
|
||||
{ duration: 8000, closable: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddMissingVariable = (varName: string) => {
|
||||
tempVarsManager.setVariable(varName, '')
|
||||
toast.success(t('variableDetection.addSuccess', { name: varName }))
|
||||
}
|
||||
// handleVariableExtracted / handleAddMissingVariable are provided by useVariableAwareInputBridge
|
||||
|
||||
// Session store(单一真源)
|
||||
const session = useImageImage2ImageSession()
|
||||
@@ -1267,14 +1251,6 @@ const getVariantImageTestId = (id: TestVariantId) => {
|
||||
const getVariantResult = (id: TestVariantId) => variantResults.value[id]
|
||||
const hasVariantResult = (id: TestVariantId) => !!(variantResults.value[id]?.images?.length)
|
||||
|
||||
const hashString = (input: string): string => {
|
||||
let hash = 5381
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
hash = ((hash << 5) + hash) ^ input.charCodeAt(i)
|
||||
}
|
||||
return (hash >>> 0).toString(36)
|
||||
}
|
||||
|
||||
// image 模式变量优先级:global < temporary < predefined
|
||||
const mergedGenerationVariables = computed<Record<string, string>>(() => ({
|
||||
...(variableManager?.customVariables.value || {}),
|
||||
@@ -1292,15 +1268,6 @@ const buildRuntimePredefinedVariables = (resolved: ResolvedPrompt): Record<strin
|
||||
}
|
||||
}
|
||||
|
||||
const stableStringifyVars = (vars: Record<string, string>): string => {
|
||||
const keys = Object.keys(vars).sort()
|
||||
let out = ''
|
||||
for (const k of keys) {
|
||||
out += `${k}=${vars[k] ?? ''}\n`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 仅用于 stale 检测:避免对完整 base64 扫描(可能很大)
|
||||
const getInputImageSignature = (): string => {
|
||||
const b64 = inputImageB64.value
|
||||
@@ -1327,7 +1294,7 @@ const getVariantFingerprint = (id: TestVariantId) => {
|
||||
...mergedGenerationVariables.value,
|
||||
...buildRuntimePredefinedVariables(resolved),
|
||||
}
|
||||
const varsHash = hashString(stableStringifyVars(varsForFingerprint))
|
||||
const varsHash = hashVariables(varsForFingerprint)
|
||||
return `${String(selection)}:${resolved.resolvedVersion}:${modelKey}:${promptHash}:${varsHash}:${imgSig}`
|
||||
}
|
||||
|
||||
@@ -1356,13 +1323,17 @@ const getVariantRequest = (id: TestVariantId): Image2ImageRequest | null => {
|
||||
...buildRuntimePredefinedVariables(resolved),
|
||||
}
|
||||
|
||||
const missing = findMissingVariables(resolved.text, varsForRequest)
|
||||
if (missing.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.missingVariables', { vars: missing.join(', ') }))
|
||||
const ctx = buildPromptExecutionContext(resolved.text, varsForRequest)
|
||||
if (ctx.forbiddenTemplateSyntax.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.forbiddenTemplateSyntax'))
|
||||
return null
|
||||
}
|
||||
if (ctx.missingVariables.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.missingVariables', { vars: ctx.missingVariables.join(', ') }))
|
||||
return null
|
||||
}
|
||||
|
||||
const prompt = replaceVariablesInContent(resolved.text, varsForRequest)
|
||||
const prompt = ctx.renderedContent
|
||||
if (!prompt.trim()) {
|
||||
toast.error(t('imageWorkspace.generation.missingRequiredFields'))
|
||||
return null
|
||||
|
||||
@@ -342,6 +342,11 @@
|
||||
<!-- 右侧:图像生成测试区域(图像模型,多列 variants) -->
|
||||
<div ref="testPaneRef" class="split-pane" style="min-width: 0; height: 100%; overflow: hidden;">
|
||||
<NFlex vertical :style="{ height: '100%', gap: '12px' }">
|
||||
<TemporaryVariablesPanel
|
||||
:manager="temporaryVariablePanelManager"
|
||||
:disabled="isOptimizing"
|
||||
:show-open-global-variables="false"
|
||||
/>
|
||||
<!-- 顶部:列数与全局操作 -->
|
||||
<NCard size="small" :style="{ flexShrink: 0 }">
|
||||
<div class="test-area-top">
|
||||
@@ -619,10 +624,16 @@ import type { SelectOption } from "../../types/select-options";
|
||||
import { useToast } from "../../composables/ui/useToast";
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { VariableAwareInput } from '../variable-extraction'
|
||||
import TemporaryVariablesPanel from '../variable/TemporaryVariablesPanel.vue'
|
||||
import { useTemporaryVariables } from '../../composables/variable/useTemporaryVariables'
|
||||
import { useVariableAwareInputBridge } from '../../composables/variable/useVariableAwareInputBridge'
|
||||
import { useTestVariableManager } from '../../composables/variable/useTestVariableManager'
|
||||
import type { VariableManagerHooks } from '../../composables/prompt/useVariableManager'
|
||||
import { platform } from '../../utils/platform'
|
||||
import { findMissingVariables, replaceVariablesInContent } from '../../utils/prompt-variables'
|
||||
import {
|
||||
buildPromptExecutionContext,
|
||||
hashString,
|
||||
hashVariables,
|
||||
} from '../../utils/prompt-variables'
|
||||
import {
|
||||
useImageText2ImageSession,
|
||||
type TestColumnCount,
|
||||
@@ -665,76 +676,49 @@ const services = inject<Ref<AppServices | null>>("services", ref(null));
|
||||
const variableManager = inject<VariableManagerHooks | null>('variableManager', null)
|
||||
const tempVarsManager = useTemporaryVariables()
|
||||
|
||||
// VariableAwareInput 需要的变量数据
|
||||
const purePredefinedVariables = computed(() => {
|
||||
const all = variableManager?.allVariables.value || {}
|
||||
const custom = variableManager?.customVariables.value || {}
|
||||
const predefined: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(all)) {
|
||||
if (!(key in custom)) predefined[key] = value
|
||||
}
|
||||
return predefined
|
||||
const {
|
||||
variableInputData,
|
||||
predefinedVariableValues: purePredefinedVariables,
|
||||
handleVariableExtracted,
|
||||
handleAddMissingVariable,
|
||||
} = useVariableAwareInputBridge({
|
||||
enabled: computed(() => true),
|
||||
// 仅在全局变量管理器就绪时启用变量感知输入(避免“变量未加载但编辑器已替换/提取”)
|
||||
isReady: computed(() => variableManager?.isReady.value ?? false),
|
||||
globalVariables: computed(() => variableManager?.customVariables.value || {}),
|
||||
temporaryVariables: tempVarsManager.temporaryVariables,
|
||||
allVariables: computed(() => variableManager?.allVariables.value || {}),
|
||||
saveGlobalVariable: (name, value) => variableManager?.addVariable(name, value),
|
||||
saveTemporaryVariable: (name, value) => tempVarsManager.setVariable(name, value),
|
||||
logPrefix: 'ImageText2ImageWorkspace',
|
||||
})
|
||||
|
||||
const variableInputData = computed(() => {
|
||||
// 仅在全局变量管理器就绪时启用变量感知输入(避免“变量未加载但编辑器已替换/提取”)
|
||||
if (!variableManager?.isReady.value) return null
|
||||
|
||||
return {
|
||||
existingGlobalVariables: Object.keys(variableManager.customVariables.value || {}),
|
||||
existingTemporaryVariables: Object.keys(tempVarsManager.temporaryVariables.value || {}),
|
||||
predefinedVariables: Object.keys(purePredefinedVariables.value || {}),
|
||||
globalVariableValues: variableManager.customVariables.value || {},
|
||||
temporaryVariableValues: tempVarsManager.temporaryVariables.value || {},
|
||||
predefinedVariableValues: purePredefinedVariables.value || {},
|
||||
}
|
||||
const temporaryVariablePanelManager = useTestVariableManager({
|
||||
globalVariables: computed(() => variableManager?.customVariables.value || {}),
|
||||
predefinedVariables: purePredefinedVariables,
|
||||
temporaryVariables: computed(() => tempVarsManager.temporaryVariables.value),
|
||||
onVariableChange: (name, value) => {
|
||||
tempVarsManager.setVariable(name, value)
|
||||
},
|
||||
onVariableRemove: (name) => {
|
||||
tempVarsManager.deleteVariable(name)
|
||||
},
|
||||
onVariablesClear: () => {
|
||||
tempVarsManager.clearAll()
|
||||
},
|
||||
onSaveToGlobal: (name, value) => {
|
||||
if (!variableManager || !variableManager.isReady.value) {
|
||||
throw new Error('variable manager not ready')
|
||||
}
|
||||
variableManager.addVariable(name, value)
|
||||
},
|
||||
})
|
||||
|
||||
const handleOriginalPromptInput = (value: string) => {
|
||||
originalPrompt.value = value
|
||||
}
|
||||
|
||||
const handleVariableExtracted = (data: {
|
||||
variableName: string
|
||||
variableValue: string
|
||||
variableType: 'global' | 'temporary'
|
||||
}) => {
|
||||
if (data.variableType === 'global') {
|
||||
try {
|
||||
variableManager?.addVariable(data.variableName, data.variableValue)
|
||||
toast.success(t('variableExtraction.savedToGlobal', { name: data.variableName }))
|
||||
} catch (error) {
|
||||
console.error('[ImageText2ImageWorkspace] Failed to save global variable:', error)
|
||||
toast.error(
|
||||
t('variableExtraction.saveFailedWithUndo', {
|
||||
name: data.variableName,
|
||||
undo: platform.getUndoKey(),
|
||||
}),
|
||||
{ duration: 8000, closable: true },
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
tempVarsManager.setVariable(data.variableName, data.variableValue)
|
||||
toast.success(t('variableExtraction.savedToTemporary', { name: data.variableName }))
|
||||
} catch (error) {
|
||||
console.error('[ImageText2ImageWorkspace] Failed to save temporary variable:', error)
|
||||
toast.error(
|
||||
t('variableExtraction.saveFailedWithUndo', {
|
||||
name: data.variableName,
|
||||
undo: platform.getUndoKey(),
|
||||
}),
|
||||
{ duration: 8000, closable: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddMissingVariable = (varName: string) => {
|
||||
tempVarsManager.setVariable(varName, '')
|
||||
toast.success(t('variableDetection.addSuccess', { name: varName }))
|
||||
}
|
||||
// handleVariableExtracted / handleAddMissingVariable are provided by useVariableAwareInputBridge
|
||||
|
||||
// Session store(单一真源)
|
||||
const session = useImageText2ImageSession()
|
||||
@@ -1122,14 +1106,6 @@ const getVariantImageTestId = (id: TestVariantId) => {
|
||||
const getVariantResult = (id: TestVariantId) => variantResults.value[id]
|
||||
const hasVariantResult = (id: TestVariantId) => !!(variantResults.value[id]?.images?.length)
|
||||
|
||||
const hashString = (input: string): string => {
|
||||
let hash = 5381
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
hash = ((hash << 5) + hash) ^ input.charCodeAt(i)
|
||||
}
|
||||
return (hash >>> 0).toString(36)
|
||||
}
|
||||
|
||||
// image 模式变量优先级:global < temporary < predefined
|
||||
// - predefined 作为只读“系统变量”,在替换阶段优先级最高
|
||||
const mergedGenerationVariables = computed<Record<string, string>>(() => ({
|
||||
@@ -1149,15 +1125,6 @@ const buildRuntimePredefinedVariables = (resolved: ResolvedPrompt): Record<strin
|
||||
}
|
||||
}
|
||||
|
||||
const stableStringifyVars = (vars: Record<string, string>): string => {
|
||||
const keys = Object.keys(vars).sort()
|
||||
let out = ''
|
||||
for (const k of keys) {
|
||||
out += `${k}=${vars[k] ?? ''}\n`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const getVariantFingerprint = (id: TestVariantId) => {
|
||||
const selection = variantVersionModels[id].value
|
||||
const resolved = resolvePromptForSelection(selection)
|
||||
@@ -1167,7 +1134,7 @@ const getVariantFingerprint = (id: TestVariantId) => {
|
||||
...mergedGenerationVariables.value,
|
||||
...buildRuntimePredefinedVariables(resolved),
|
||||
}
|
||||
const varsHash = hashString(stableStringifyVars(varsForFingerprint))
|
||||
const varsHash = hashVariables(varsForFingerprint)
|
||||
return `${String(selection)}:${resolved.resolvedVersion}:${modelKey}:${promptHash}:${varsHash}`
|
||||
}
|
||||
|
||||
@@ -1196,13 +1163,17 @@ const getVariantRequest = (id: TestVariantId): Text2ImageRequest | null => {
|
||||
...buildRuntimePredefinedVariables(resolved),
|
||||
}
|
||||
|
||||
const missing = findMissingVariables(resolved.text, varsForRequest)
|
||||
if (missing.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.missingVariables', { vars: missing.join(', ') }))
|
||||
const ctx = buildPromptExecutionContext(resolved.text, varsForRequest)
|
||||
if (ctx.forbiddenTemplateSyntax.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.forbiddenTemplateSyntax'))
|
||||
return null
|
||||
}
|
||||
if (ctx.missingVariables.length > 0) {
|
||||
toast.error(t('imageWorkspace.generation.missingVariables', { vars: ctx.missingVariables.join(', ') }))
|
||||
return null
|
||||
}
|
||||
|
||||
const prompt = replaceVariablesInContent(resolved.text, varsForRequest)
|
||||
const prompt = ctx.renderedContent
|
||||
if (!prompt.trim()) {
|
||||
toast.error(t('imageWorkspace.generation.missingRequiredFields'))
|
||||
return null
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<!-- 变量值输入表单(临时变量编辑区) -->
|
||||
<NCard
|
||||
:title="t('test.variables.formTitle')"
|
||||
size="small"
|
||||
:bordered="true"
|
||||
:style="{ flexShrink: 0 }"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NSpace :size="8">
|
||||
<NTag :bordered="false" type="info" size="small">
|
||||
{{
|
||||
t('test.variables.tempCount', {
|
||||
count: displayVariables.length,
|
||||
})
|
||||
}}
|
||||
</NTag>
|
||||
<NButton
|
||||
v-if="props.showGenerateValues"
|
||||
size="small"
|
||||
quaternary
|
||||
:loading="props.isGenerating"
|
||||
:disabled="
|
||||
props.disabled ||
|
||||
props.isGenerating ||
|
||||
displayVariables.length === 0
|
||||
"
|
||||
@click="emit('generate-values')"
|
||||
>
|
||||
{{
|
||||
props.isGenerating
|
||||
? t('test.variableValueGeneration.generating')
|
||||
: t('test.variableValueGeneration.generateButton')
|
||||
}}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
:disabled="props.disabled"
|
||||
@click="handleClearAllVariables"
|
||||
>
|
||||
{{ t('test.variables.clearAll') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
v-if="props.showOpenGlobalVariables"
|
||||
size="small"
|
||||
quaternary
|
||||
:disabled="props.disabled"
|
||||
@click="emit('open-global-variables')"
|
||||
>
|
||||
{{ t('contextMode.actions.globalVariables') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<NSpace vertical :size="12">
|
||||
<!-- 变量输入项 -->
|
||||
<div
|
||||
v-for="varName in displayVariables"
|
||||
:key="varName"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}"
|
||||
>
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:type="
|
||||
getVariableSource(varName) === 'predefined'
|
||||
? 'success'
|
||||
: getVariableSource(varName) === 'test'
|
||||
? 'warning'
|
||||
: 'default'
|
||||
"
|
||||
:style="{ minWidth: '120px', flexShrink: 0 }"
|
||||
>
|
||||
<span v-text="`{{${varName}}}`"></span>
|
||||
</NTag>
|
||||
<NInput
|
||||
:value="getVariableDisplayValue(varName)"
|
||||
:placeholder="getVariablePlaceholder(varName)"
|
||||
size="small"
|
||||
:disabled="props.disabled"
|
||||
:style="{ flex: 1 }"
|
||||
@update:value="handleVariableValueChange(varName, $event)"
|
||||
/>
|
||||
<!-- 删除按钮 (仅临时变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
:disabled="props.disabled"
|
||||
@click="handleDeleteVariable(varName)"
|
||||
:title="t('test.variables.delete')"
|
||||
>
|
||||
🗑️
|
||||
</NButton>
|
||||
<!-- 保存到全局按钮 (仅测试变量显示) -->
|
||||
<NButton
|
||||
v-if="getVariableSource(varName) === 'test'"
|
||||
size="small"
|
||||
quaternary
|
||||
:disabled="props.disabled"
|
||||
@click="handleSaveToGlobal(varName)"
|
||||
:title="t('test.variables.saveToGlobal')"
|
||||
>
|
||||
💾
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- 无变量提示 -->
|
||||
<NEmpty
|
||||
v-if="displayVariables.length === 0"
|
||||
:description="t('test.variables.noVariables')"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<NSpace :size="8" justify="end">
|
||||
<!-- 添加变量按钮 -->
|
||||
<NButton
|
||||
size="small"
|
||||
:disabled="props.disabled"
|
||||
@click="showAddVariableDialog = true"
|
||||
>
|
||||
{{ t('test.variables.addVariable') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 添加变量对话框 -->
|
||||
<NModal
|
||||
v-model:show="showAddVariableDialog"
|
||||
preset="dialog"
|
||||
:title="t('test.variables.addVariable')"
|
||||
:positive-text="t('common.confirm')"
|
||||
:negative-text="t('common.cancel')"
|
||||
:on-positive-click="handleAddVariable"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<NSpace vertical :size="12" style="margin-top: 16px">
|
||||
<NFormItem
|
||||
:label="t('variableExtraction.variableName')"
|
||||
:validation-status="newVariableNameError ? 'error' : undefined"
|
||||
:feedback="newVariableNameError"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="newVariableName"
|
||||
:placeholder="t('variableExtraction.variableNamePlaceholder')"
|
||||
:disabled="props.disabled"
|
||||
@input="validateNewVariableName"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('variableExtraction.variableValue')">
|
||||
<NInput
|
||||
v-model:value="newVariableValue"
|
||||
:placeholder="t('variableExtraction.variableValuePlaceholder')"
|
||||
:disabled="props.disabled"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
NCard,
|
||||
NSpace,
|
||||
NTag,
|
||||
NButton,
|
||||
NInput,
|
||||
NEmpty,
|
||||
NModal,
|
||||
NFormItem,
|
||||
} from 'naive-ui'
|
||||
|
||||
import type { TestVariableManager } from '../../composables/variable/useTestVariableManager'
|
||||
|
||||
interface Props {
|
||||
manager: TestVariableManager
|
||||
disabled?: boolean
|
||||
|
||||
// Optional actions
|
||||
showOpenGlobalVariables?: boolean
|
||||
showGenerateValues?: boolean
|
||||
isGenerating?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
showOpenGlobalVariables: true,
|
||||
showGenerateValues: false,
|
||||
isGenerating: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-global-variables'): void
|
||||
(e: 'generate-values'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
showAddVariableDialog,
|
||||
newVariableName,
|
||||
newVariableValue,
|
||||
newVariableNameError,
|
||||
sortedVariables,
|
||||
getVariableSource,
|
||||
getVariableDisplayValue,
|
||||
getVariablePlaceholder,
|
||||
validateNewVariableName,
|
||||
handleVariableValueChange,
|
||||
handleAddVariable,
|
||||
handleDeleteVariable,
|
||||
handleClearAllVariables,
|
||||
handleSaveToGlobal,
|
||||
} = props.manager
|
||||
|
||||
const displayVariables = computed(() => sortedVariables.value)
|
||||
</script>
|
||||
@@ -8,6 +8,8 @@ import { ref, computed, watch, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
import { VARIABLE_VALIDATION } from '../../types/variable'
|
||||
|
||||
interface TestVariable {
|
||||
value: string
|
||||
timestamp: number
|
||||
@@ -34,6 +36,9 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const hasOwn = (obj: Record<string, unknown>, key: string) =>
|
||||
Object.prototype.hasOwnProperty.call(obj, key)
|
||||
|
||||
// 添加变量对话框状态
|
||||
const showAddVariableDialog = ref(false)
|
||||
const newVariableName = ref('')
|
||||
@@ -87,10 +92,15 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
})
|
||||
|
||||
// 获取变量来源
|
||||
const getVariableSource = (varName: string): 'predefined' | 'test' | 'global' | 'empty' => {
|
||||
if (options.predefinedVariables.value[varName]) return 'predefined'
|
||||
if (testVariables.value[varName]) return 'test'
|
||||
if (options.globalVariables.value[varName]) return 'global'
|
||||
const getVariableSource = (
|
||||
varName: string
|
||||
): 'predefined' | 'test' | 'global' | 'empty' => {
|
||||
// Use key existence (not truthiness) because empty-string values are valid.
|
||||
if (hasOwn(options.predefinedVariables.value, varName)) return 'predefined'
|
||||
if (hasOwn(testVariables.value as unknown as Record<string, unknown>, varName)) {
|
||||
return 'test'
|
||||
}
|
||||
if (hasOwn(options.globalVariables.value, varName)) return 'global'
|
||||
return 'empty'
|
||||
}
|
||||
|
||||
@@ -101,10 +111,10 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
|
||||
// 获取变量占位符
|
||||
const getVariablePlaceholder = (varName: string): string => {
|
||||
if (options.predefinedVariables.value[varName]) {
|
||||
if (hasOwn(options.predefinedVariables.value, varName)) {
|
||||
return t('test.variables.inputPlaceholder') + ` (${t('variables.source.predefined')})`
|
||||
}
|
||||
if (options.globalVariables.value[varName]) {
|
||||
if (hasOwn(options.globalVariables.value, varName)) {
|
||||
return t('test.variables.inputPlaceholder') + ` (${t('variables.source.global')})`
|
||||
}
|
||||
return t('test.variables.inputPlaceholder')
|
||||
@@ -113,11 +123,24 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
// 验证变量名
|
||||
const validateVariableName = (name: string): string => {
|
||||
if (!name) return ''
|
||||
if (/^\d/.test(name)) return t('variableExtraction.validation.noNumberStart')
|
||||
if (!/^[\u4e00-\u9fa5a-zA-Z_][\u4e00-\u9fa5a-zA-Z0-9_]*$/.test(name)) {
|
||||
|
||||
const trimmedName = name.trim()
|
||||
if (!trimmedName) return ''
|
||||
|
||||
// Align with shared variable rules used by scanning/replacement.
|
||||
if (!VARIABLE_VALIDATION.NAME_PATTERN.test(trimmedName)) {
|
||||
return t('variableExtraction.validation.invalidCharacters')
|
||||
}
|
||||
if (testVariables.value[name]) return t('variableExtraction.validation.duplicateVariable')
|
||||
|
||||
// Prevent creating a temporary variable that would be shadowed by predefined variables.
|
||||
if (hasOwn(options.predefinedVariables.value, trimmedName)) {
|
||||
return t('variableExtraction.validation.predefinedVariable')
|
||||
}
|
||||
|
||||
if (testVariables.value[trimmedName]) {
|
||||
return t('variableExtraction.validation.duplicateVariable')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -161,15 +184,31 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
// 删除变量
|
||||
const handleDeleteVariable = (varName: string) => {
|
||||
delete testVariables.value[varName]
|
||||
options.onVariableRemove?.(varName)
|
||||
options.onVariableChange?.(varName, '')
|
||||
|
||||
// Prefer explicit remove callback; fallback to onVariableChange(name, '') for legacy callers.
|
||||
if (options.onVariableRemove) {
|
||||
options.onVariableRemove(varName)
|
||||
} else {
|
||||
options.onVariableChange?.(varName, '')
|
||||
}
|
||||
|
||||
message.success(t('test.variables.deleteSuccess', { name: varName }))
|
||||
}
|
||||
|
||||
// 清空所有变量
|
||||
const handleClearAllVariables = () => {
|
||||
const removedNames = Object.keys(testVariables.value)
|
||||
testVariables.value = {}
|
||||
options.onVariablesClear?.()
|
||||
|
||||
// Prefer explicit clear callback; otherwise best-effort remove for callers.
|
||||
if (options.onVariablesClear) {
|
||||
options.onVariablesClear()
|
||||
} else if (options.onVariableRemove) {
|
||||
removedNames.forEach((name) => options.onVariableRemove?.(name))
|
||||
} else {
|
||||
removedNames.forEach((name) => options.onVariableChange?.(name, ''))
|
||||
}
|
||||
|
||||
message.success(t('test.variables.clearSuccess'))
|
||||
}
|
||||
|
||||
@@ -180,8 +219,22 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
message.warning(t('test.variables.emptyValueWarning'))
|
||||
return
|
||||
}
|
||||
options.onSaveToGlobal?.(varName, varData.value)
|
||||
message.success(t('test.variables.savedToGlobal'))
|
||||
|
||||
if (!options.onSaveToGlobal) return
|
||||
|
||||
try {
|
||||
options.onSaveToGlobal(varName, varData.value)
|
||||
message.success(t('test.variables.savedToGlobal'))
|
||||
} catch (err) {
|
||||
console.warn('[useTestVariableManager] onSaveToGlobal failed:', err)
|
||||
|
||||
const errMessage = err instanceof Error ? err.message : ''
|
||||
if (/not\s+ready/i.test(errMessage)) {
|
||||
message.error(t('variableExtraction.managerNotReady'))
|
||||
} else {
|
||||
message.error(t('variableExtraction.saveFailed', { name: varName }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有变量值
|
||||
@@ -219,3 +272,5 @@ export function useTestVariableManager(options: TestVariableManagerOptions) {
|
||||
setVariableValues,
|
||||
}
|
||||
}
|
||||
|
||||
export type TestVariableManager = ReturnType<typeof useTestVariableManager>
|
||||
Reference in new issue
Block a user