refactor(ui): 重构上下文模式测试面板架构,实现职责分离

- 新增 ContextUserTestPanel 组件,专门处理 Context User 模式
- 简化 TestAreaPanel,移除多模式判断逻辑,仅保留 Basic Mode
- 移除 contextMode prop,各组件职责更加清晰
- 修复 ConversationTestPanel 和 ContextUserTestPanel 的定时器内存泄漏
- 更新相关组件和类型定义,保持 API 一致性

变更影响:
- TestAreaPanel: 从 780 行减少到 380 行
- 新增 ContextUserTestPanel: 630 行专用组件
- 架构更清晰:Basic/ContextUser/ContextSystem 各自独立
This commit is contained in:
linshen committed 2025-11-22 13:38:52 +08:00
1 parent 7b5ed1ee41
commit 1970a2bac9
11 files changed
+671 -461

No files matched your search

+6 -4
View File
@@ -2146,16 +2146,18 @@ const getActiveTestPanelInstance = (): TestAreaPanelInstance | null => {
return null;
};
// 基础模式和 context-user 模式的测试处理函数
// 注意:context-system 模式已在 ContextSystemWorkspace 内部使用 useConversationTester 处理,不会调用此函数
// 基础模式的测试处理函数
// 注意:
// 1. Context System 模式在 ContextSystemWorkspace 内部使用 useConversationTester 处理
// 2. Context User 模式在 ContextUserWorkspace 内部使用 useContextUserTester 处理
// 3. 此函数仅被 Basic Mode 的 TestAreaPanel 调用
const handleTestAreaTest = async (testVariables?: Record<string, string>) => {
// 调用基础测试器(只用于基础模式和 context-user)
await promptTester.executeTest(
optimizer.prompt,
optimizer.optimizedPrompt,
testContent.value,
isCompareMode.value,
testVariables
testVariables || {}
);
};
+13 -423
View File
@@ -15,148 +15,7 @@
/>
</div>
<!-- 变量值输入表单 (完整实现) -->
<div
v-if="showVariableForm"
:style="{ flexShrink: 0, marginBottom: '16px' }"
>
<NCard
:title="t('test.variables.formTitle')"
size="small"
:bordered="true"
>
<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>
</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'
: getVariableSource(varName) === 'global'
? 'default'
: '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>
</div>
<!-- 🆕 添加变量对话框 -->
<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>
<!-- 控制工具栏 -->
<div :style="{ flexShrink: 0 }">
@@ -255,20 +114,11 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, ref, onUnmounted } from 'vue'
import { useI18n } from "vue-i18n";
import {
useMessage,
NFlex,
NCard,
NButton,
NTag,
NSpace,
NInput,
NEmpty,
NModal,
NFormItem,
} from "naive-ui";
import type {
OptimizationMode,
@@ -278,14 +128,12 @@ import type {
import { useResponsive } from '../composables/ui/useResponsive';
import { usePerformanceMonitor } from "../composables/performance/usePerformanceMonitor";
import { useDebounceThrottle } from "../composables/performance/useDebounceThrottle";
import { useCurrentMode } from "../composables/mode";
import TestInputSection from "./TestInputSection.vue";
import TestControlBar from "./TestControlBar.vue";
import TestResultSection from "./TestResultSection.vue";
import ToolCallDisplay from "./ToolCallDisplay.vue";
const { t } = useI18n();
const message = useMessage();
// 性能监控
const {
@@ -307,13 +155,9 @@ const {
// gridConfig // 保留用于网格布局
} = useResponsive();
// 🆕 模式检测(用于隐藏基础模式下的变量功能)
const { isBasicMode } = useCurrentMode();
interface Props {
// 核心状态
optimizationMode: OptimizationMode;
contextMode?: import("@prompt-optimizer/core").ContextMode;
isTestRunning?: boolean;
// 测试内容
@@ -321,10 +165,7 @@ interface Props {
optimizedPrompt?: string; // 优化后的提示词(用于变量检测)
isCompareMode?: boolean;
// 🆕 两层变量体系 (简化)
globalVariables?: Record<string, string>; // 全局自定义变量
predefinedVariables?: Record<string, string>; // 内置预定义变量
temporaryVariables?: Record<string, string>; // 🆕 临时变量 (从InputPanel提取的变量)
// 功能开关
enableCompareMode?: boolean;
@@ -349,7 +190,6 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
contextMode: "user",
isTestRunning: false,
testContent: "",
isCompareMode: true,
@@ -363,22 +203,17 @@ const props = withDefaults(defineProps<Props>(), {
originalResultTitle: "",
optimizedResultTitle: "",
singleResultTitle: "",
globalVariables: () => ({}),
predefinedVariables: () => ({}),
temporaryVariables: () => ({}),
});
const emit = defineEmits<{
"update:testContent": [value: string];
"update:isCompareMode": [value: boolean];
test: [testVariables: Record<string, string>]; // 🆕 传递测试变量
test: []; // 🆕 传递测试变量
"compare-toggle": [];
// 高级功能事件
"open-variable-manager": [];
"open-context-editor": [];
"variable-change": [name: string, value: string];
"save-to-global": [name: string, value: string]; // 🆕 保存测试变量到全局
"get-test-variables": []; // 🆕 请求获取测试变量(用于测试执行)
"context-change": [
messages: ConversationMessage[],
variables: Record<string, string>,
@@ -389,8 +224,7 @@ const emit = defineEmits<{
toolCalls: ToolCallResult[],
testType: "original" | "optimized",
];
"temporary-variable-remove": [name: string];
"temporary-variables-clear": [];
}>();
// 内部状态管理 - 去除防抖,保证输入即时响应
@@ -442,18 +276,10 @@ const clearToolCalls = (
// 移除结果缓存与相关节流逻辑,避免不必要的复杂度
// 关键计算属性:showTestInput 取决于当前功能模式
// 关键计算属性:showTestInput 取决于优化模式
// 基础模式:仅在系统提示词优化时需要测试内容输入
const showTestInput = computed(() => {
// 基础模式始终以系统提示词布尔值决定可见性
if (isBasicMode.value) {
return props.optimizationMode === "system";
}
// 上下文模式判断
// - 多轮对话模式(contextMode === "system"):不显示测试内容输入框(测试内容就是会话消息)
// - 变量模式(contextMode === "user"):显示测试内容输入框
return (
props.contextMode === "user" && props.optimizationMode === "system"
);
return props.optimizationMode === "system";
});
// 响应式布局配置
@@ -491,8 +317,7 @@ const primaryActionDisabled = computed(() => {
if (props.isTestRunning) return true;
// 系统提示词模式需要测试内容
// 但多轮对话模式(contextMode === "system")例外,因为测试内容就是会话消息
if (props.optimizationMode === "system" && props.contextMode !== "system" && !props.testContent.trim()) {
if (props.optimizationMode === "system" && !props.testContent.trim()) {
return true;
}
@@ -509,9 +334,7 @@ const handleCompareToggle = () => {
const handleTest = throttle(
() => {
// 🆕 获取并传递测试变量
const testVars = getVariableValues();
emit("test", testVars);
emit("test");
recordUpdate();
},
200,
@@ -521,243 +344,11 @@ const handleTest = throttle(
// ========== 变量管理 ==========
// 🆕 添加变量对话框状态
const showAddVariableDialog = ref(false);
const newVariableName = ref("");
const newVariableValue = ref("");
const newVariableNameError = ref("");
// 🧪 测试区临时变量 (仅内存,刷新丢失) - 新增功能
// 数据结构: { 变量名: { value: 值, timestamp: 时间戳 } }
interface TestVariable {
value: string;
timestamp: number;
}
const testVariables = ref<Record<string, TestVariable>>({});
// 监听 props.temporaryVariables 变化,同步到内部状态
watch(
() => props.temporaryVariables,
(newVars) => {
// 🔧 第一步:删除不再存在于 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(() => {
// 将 testVariables 转换为 { name: value } 格式
const testVarsFlat: Record<string, string> = {};
for (const [name, data] of Object.entries(testVariables.value)) {
testVarsFlat[name] = data.value;
}
return {
...props.globalVariables, // 优先级 1: 全局自定义变量
...testVarsFlat, // 优先级 2: 测试区临时变量
...props.predefinedVariables, // 优先级 3: 内置预定义变量
};
});
// 🆕 按时间排序的临时变量列表 (最新的在最前面)
const sortedTestVariables = computed(() => {
const entries = Object.entries(testVariables.value);
return entries
.sort((a, b) => b[1].timestamp - a[1].timestamp) // 降序排列
.map(([name]) => name);
});
// 🆕 实际显示的变量列表 = 临时变量 (不再依赖 detectedVariables)
const displayVariables = computed(() => {
return sortedTestVariables.value;
});
// 是否显示变量表单:默认显示(基础模式除外)
const showVariableForm = computed(() => {
// 🆕 基础模式不显示变量功能(变量系统仅在上下文模式下可用)
if (isBasicMode.value) {
return false;
}
// 测试运行期间也应保持显示,避免界面闪烁和信息丢失
return true;
});
// 获取变量的显示值(从合并后的变量中获取)
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");
};
// 变量列表变化时的清理逻辑已不再需要(不再使用 userInputValues)
// 事件处理函数
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;
}
// 验证规则1: 不能以数字开头
if (/^\d/.test(name)) {
newVariableNameError.value = t(
"variableExtraction.validation.noNumberStart"
);
return false;
}
// 验证规则2: 只能包含中文、英文、数字、下划线
if (!/^[\u4e00-\u9fa5a-zA-Z_][\u4e00-\u9fa5a-zA-Z0-9_]*$/.test(name)) {
newVariableNameError.value = t(
"variableExtraction.validation.invalidCharacters"
);
return false;
}
// 验证规则3: 不能与已有变量重名
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 };
};
// 设置变量值(外部调用)- 通过 emit 同步到会话变量
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";
};
// 移除未使用的 props 变化防抖处理,避免多余复杂度
// 开发环境下的性能调试
if (import.meta.env.DEV) {
@@ -774,7 +365,8 @@ if (import.meta.env.DEV) {
);
// 定期检查性能
setInterval(logPerformance, 10000);
const timer = setInterval(logPerformance, 10000);
onUnmounted(() => clearInterval(timer));
}
// 暴露方法供父组件调用
@@ -786,9 +378,7 @@ defineExpose({
original: originalToolCalls.value,
optimized: optimizedToolCalls.value,
}),
// 变量管理
getVariableValues,
setVariableValues,
});
</script>
@@ -1,7 +1,7 @@
<template>
<NFlex align="center" :wrap="false" :size="12">
<!-- 快捷操作按钮 -->
<!-- 全局变量 - 两种模式都显示 -->
<!-- 全局变量 -->
<NButton
size="small"
type="default"
@@ -14,9 +14,8 @@
{{ $t('contextMode.actions.globalVariables') }}
</NButton>
<!-- 工具管理 - 仅用户模式显示 -->
<!-- 工具管理 -->
<NButton
v-if="contextMode === 'user'"
size="small"
type="default"
@click="$emit('open-tool-manager')"
@@ -32,11 +31,6 @@
<script setup lang="ts">
import { NButton, NFlex } from 'naive-ui'
import { type ContextMode } from '@prompt-optimizer/core'
const props = defineProps<{
contextMode: ContextMode
}>()
const emit = defineEmits<{
'open-global-variables': []
@@ -32,7 +32,6 @@
:available-variables="availableVariables"
:scan-variables="scanVariables"
:optimization-mode="optimizationMode"
context-mode="system"
:tool-count="toolCount"
@open-variable-manager="emit('open-variable-manager')"
@open-context-editor="emit('open-context-editor')"
@@ -0,0 +1,629 @@
<template>
<NFlex vertical :style="{ height: '100%' }">
<!-- 测试内容输入区 (ContextUser专属,ConversationTestPanel没有这个) -->
<div :style="{ flexShrink: 0, marginBottom: '16px' }">
<TestInputSection
v-model="testContentProxy"
:label="t('test.content')"
:placeholder="t('test.simpleMode.placeholder')"
:disabled="isTestRunning"
:mode="inputMode"
:size="inputSize"
:enable-fullscreen="enableFullscreen"
/>
</div>
<!-- 变量值输入表单 -->
<div
v-if="showVariableForm"
:style="{ flexShrink: 0, marginBottom: '16px' }"
>
<NCard
:title="t('test.variables.formTitle')"
size="small"
:bordered="true"
>
<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>
</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>
</div>
<!-- 添加变量对话框 -->
<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>
<!-- 控制工具栏 -->
<div :style="{ flexShrink: 0 }">
<TestControlBar
:model-label="t('test.model')"
:show-compare-toggle="enableCompareMode"
:is-compare-mode="isCompareMode"
@update:is-compare-mode="handleCompareModeToggle"
:primary-action-text="primaryActionText"
:primary-action-disabled="primaryActionDisabled"
:primary-action-loading="isTestRunning"
:layout="adaptiveControlBarLayout"
:button-size="adaptiveButtonSize"
@primary-action="handleTest"
:style="{ marginBottom: '16px' }"
>
<template #model-select>
<slot name="model-select"></slot>
</template>
<template #secondary-controls>
<slot name="secondary-controls"></slot>
</template>
<template #custom-actions>
<slot name="custom-actions"></slot>
</template>
</TestControlBar>
</div>
<!-- 测试结果区域(不支持工具调用,仅显示文本结果)-->
<TestResultSection
:is-compare-mode="isCompareMode"
:vertical-layout="adaptiveResultVerticalLayout"
:show-original="isCompareMode"
:original-result-title="t('test.originalResult')"
:optimized-result-title="t('test.optimizedResult')"
:single-result-title="singleResultTitle"
:size="adaptiveButtonSize"
:style="{ flex: 1, minHeight: 0 }"
>
<!-- 对比模式:原始结果 -->
<template #original-result>
<slot name="original-result"></slot>
</template>
<!-- 对比模式:优化结果 -->
<template #optimized-result>
<slot name="optimized-result"></slot>
</template>
<!-- 单一结果模式 -->
<template #single-result>
<slot name="single-result"></slot>
</template>
</TestResultSection>
</NFlex>
</template>
<script setup lang="ts">
import { computed, ref, watch, onUnmounted } from 'vue'
import { useI18n } from "vue-i18n";
import {
useMessage,
NFlex,
NCard,
NButton,
NTag,
NSpace,
NInput,
NEmpty,
NModal,
NFormItem,
} from "naive-ui";
import { useResponsive } from '../../composables/ui/useResponsive';
import { usePerformanceMonitor } from "../../composables/performance/usePerformanceMonitor";
import { useDebounceThrottle } from "../../composables/performance/useDebounceThrottle";
import TestInputSection from "../TestInputSection.vue";
import TestControlBar from "../TestControlBar.vue";
import TestResultSection from "../TestResultSection.vue";
const { t } = useI18n();
const message = useMessage();
// 性能监控
const { recordUpdate, getPerformanceReport } = usePerformanceMonitor("ContextUserTestPanel");
// 防抖节流
const { debounce, throttle } = useDebounceThrottle();
// 响应式配置
const {
shouldUseVerticalLayout,
shouldUseCompactMode,
buttonSize,
inputSize,
} = useResponsive();
interface Props {
// 测试内容(ContextUser专属)
testContent: string;
// 优化后的提示词(用于检测变量)
optimizedPrompt?: string;
// 测试状态
isTestRunning?: boolean;
isCompareMode?: boolean;
enableCompareMode?: boolean;
// 变量管理(三层)
globalVariables?: Record<string, string>;
predefinedVariables?: Record<string, string>;
temporaryVariables?: Record<string, string>;
// 功能开关
enableFullscreen?: boolean;
// 布局配置
inputMode?: "compact" | "normal";
controlBarLayout?: "default" | "compact" | "minimal";
buttonSize?: "small" | "medium" | "large";
resultVerticalLayout?: boolean;
// 结果显示配置
singleResultTitle?: string;
}
const props = withDefaults(defineProps<Props>(), {
optimizedPrompt: "",
isTestRunning: false,
isCompareMode: false,
enableCompareMode: true,
enableFullscreen: true,
inputMode: "normal",
controlBarLayout: "default",
buttonSize: "medium",
resultVerticalLayout: false,
singleResultTitle: "",
globalVariables: () => ({}),
predefinedVariables: () => ({}),
temporaryVariables: () => ({}),
});
const emit = defineEmits<{
"update:testContent": [value: string];
"update:isCompareMode": [value: boolean];
test: [testVariables: Record<string, string>];
"compare-toggle": [];
"open-variable-manager": [];
"variable-change": [name: string, value: string];
"save-to-global": [name: string, value: string];
"temporary-variable-remove": [name: string];
"temporary-variables-clear": [];
}>();
// 测试内容双向绑定
const testContentProxy = computed({
get: () => props.testContent,
set: (value: string) => {
emit("update:testContent", value);
recordUpdate();
},
});
// 处理对比模式切换
const handleCompareModeToggle = (value: boolean) => {
emit("update:isCompareMode", value);
emit("compare-toggle");
recordUpdate();
};
// 响应式布局配置
const adaptiveControlBarLayout = computed(() => {
if (shouldUseCompactMode.value) return "minimal";
if (shouldUseVerticalLayout.value) return "compact";
return props.controlBarLayout || "default";
});
const adaptiveButtonSize = computed(() => {
return buttonSize.value;
});
const adaptiveResultVerticalLayout = computed(() => {
return shouldUseVerticalLayout.value || props.resultVerticalLayout;
});
// 主要操作按钮文本
const primaryActionText = computed(() => {
if (props.isTestRunning) {
return t("test.testing");
}
return props.isCompareMode
? t("test.startCompare")
: t("test.startTest");
});
// 主要操作按钮禁用状态(纯业务逻辑,无模式判断)
const primaryActionDisabled = computed(() => {
if (props.isTestRunning) return true;
if (!props.testContent.trim()) return true; // 测试内容不能为空
return false;
});
const handleTest = throttle(
() => {
// 获取并传递测试变量
const testVars = getVariableValues();
emit("test", testVars);
recordUpdate();
},
200,
"handleTest",
);
// ========== 变量管理 ==========
// 添加变量对话框状态
const showAddVariableDialog = ref(false);
const newVariableName = ref("");
const newVariableValue = ref("");
const newVariableNameError = ref("");
// 测试区临时变量
interface TestVariable {
value: string;
timestamp: number;
}
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 showVariableForm = computed(() => {
// ✅ 始终显示,避免测试期间闪烁(已修复的bug)
return true;
});
// 获取变量的显示值
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";
};
// 开发环境下的性能调试
if (import.meta.env.DEV) {
const logPerformance = debounce(
() => {
const report = getPerformanceReport();
if (report.grade.grade === "F") {
console.warn("ContextUserTestPanel 性能较差:", report);
}
},
5000,
false,
"performanceLog",
);
const timer = setInterval(logPerformance, 10000);
onUnmounted(() => clearInterval(timer));
}
// 暴露方法供父组件调用(兼容 TestAreaPanelInstance 接口)
defineExpose({
// ContextUser 不支持工具调用,提供空实现
clearToolCalls: () => {},
handleToolCall: () => {},
getToolCalls: () => ({ original: [], optimized: [] }),
// 变量管理
getVariableValues,
setVariableValues,
// 预览功能占位符(兼容接口)
showPreview: () => {
console.warn('[ContextUserTestPanel] showPreview not implemented');
},
hidePreview: () => {
console.warn('[ContextUserTestPanel] hidePreview not implemented');
},
});
</script>
<style scoped>
/* ContextUser 不需要工具调用相关样式 */
</style>
@@ -141,27 +141,23 @@
style="flex: 1; overflow: auto"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<TestAreaPanel
<ContextUserTestPanel
ref="testAreaPanelRef"
:optimization-mode="optimizationMode"
context-mode="user"
:test-content="testContent"
@update:testContent="emit('update:testContent', $event)"
:optimized-prompt="contextUserOptimization.optimizedPrompt"
:is-test-running="contextUserTester.testResults.isTestingOriginal || contextUserTester.testResults.isTestingOptimized"
:is-compare-mode="isCompareMode"
@update:isCompareMode="emit('update:isCompareMode', $event)"
:global-variables="globalVariables"
:predefined-variables="predefinedVariables"
:temporary-variables="temporaryVariables"
:testContent="testContent"
@update:testContent="emit('update:testContent', $event)"
:isCompareMode="isCompareMode"
@update:isCompareMode="emit('update:isCompareMode', $event)"
:enable-compare-mode="true"
:enable-fullscreen="true"
:input-mode="inputMode"
:control-bar-layout="controlBarLayout"
:button-size="buttonSize"
:conversation-max-height="conversationMaxHeight"
:show-original-result="true"
:result-vertical-layout="resultVerticalLayout"
:single-result-title="t('test.testResult')"
@test="handleTestWithVariables"
@compare-toggle="emit('compare-toggle')"
@open-variable-manager="emit('open-variable-manager')"
@@ -212,7 +208,7 @@
:style="{ height: '100%', minHeight: '0' }"
/>
</template>
</TestAreaPanel>
</ContextUserTestPanel>
</NCard>
</NFlex>
</NFlex>
@@ -254,7 +250,7 @@ import { NCard, NFlex, NButton, NText } from "naive-ui";
import { useBreakpoints } from "@vueuse/core";
import InputPanelUI from "../InputPanel.vue";
import PromptPanelUI from "../PromptPanel.vue";
import TestAreaPanel from "../TestAreaPanel.vue";
import ContextUserTestPanel from "./ContextUserTestPanel.vue";
import OutputDisplay from "../OutputDisplay.vue";
import type { OptimizationMode } from "../../types";
import type {
@@ -394,7 +394,6 @@ const props = withDefaults(defineProps<ConversationManagerProps>(), {
title: undefined,
toolCount: 0,
optimizationMode: "system",
contextMode: "system",
scanVariables: () => [],
replaceVariables: (content: string) => content,
isPredefinedVariable: () => false,
@@ -468,13 +467,11 @@ const scrollbarStyle = computed(() => {
return {};
});
// 模式化行为:用户模式隐藏消息增删改按钮,系统模式保留完整能力
// 消息编辑权限控制
const canEditMessages = computed(() => {
// readonly优先级最高
if (props.readonly) return false;
// 用户模式不允许编辑消息
if (props.contextMode === "user") return false;
// 系统模式允许编辑
// 允许编辑
return true;
});
@@ -242,7 +242,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, onUnmounted } from 'vue'
import { useI18n } from "vue-i18n";
import {
@@ -656,7 +656,8 @@ if (import.meta.env.DEV) {
"performanceLog",
);
setInterval(logPerformance, 10000);
const timer = setInterval(logPerformance, 10000);
onUnmounted(() => clearInterval(timer));
}
// 暴露方法供父组件调用(兼容 TestAreaPanelInstance 接口)
+2
View File
@@ -99,6 +99,8 @@ export { default as ContextModeActions } from "./components/context-mode/Context
export { default as PromptPreviewPanel } from "./components/PromptPreviewPanel.vue";
export { default as ContextSystemWorkspace } from "./components/context-mode/ContextSystemWorkspace.vue";
export { default as ContextUserWorkspace } from "./components/context-mode/ContextUserWorkspace.vue";
export { default as ContextUserTestPanel } from "./components/context-mode/ContextUserTestPanel.vue";
export { default as ConversationTestPanel } from "./components/context-mode/ConversationTestPanel.vue";
// 导出 Naive UI 组件 (解决组件解析问题)
export {
-2
View File
@@ -85,8 +85,6 @@ export interface ConversationManagerProps extends BaseComponentProps {
availableVariables: Record<string, string>
/** 优化模式(用于模板分类) */
optimizationMode?: 'system' | 'user'
/** 上下文模式(用于控制UI行为) */
contextMode?: import('@prompt-optimizer/core').ContextMode
/** 变量扫描函数(标准化注入) */
scanVariables?: (content: string) => string[]
/** 变量替换函数(标准化注入) */
+6 -4
View File
@@ -2146,16 +2146,18 @@ const getActiveTestPanelInstance = (): TestAreaPanelInstance | null => {
return null;
};
// 基础模式和 context-user 模式的测试处理函数
// 注意:context-system 模式已在 ContextSystemWorkspace 内部使用 useConversationTester 处理,不会调用此函数
// 基础模式的测试处理函数
// 注意:
// 1. Context System 模式在 ContextSystemWorkspace 内部使用 useConversationTester 处理
// 2. Context User 模式在 ContextUserWorkspace 内部使用 useContextUserTester 处理
// 3. 此函数仅被 Basic Mode 的 TestAreaPanel 调用
const handleTestAreaTest = async (testVariables?: Record<string, string>) => {
// 调用基础测试器(只用于基础模式和 context-user)
await promptTester.executeTest(
optimizer.prompt,
optimizer.optimizedPrompt,
testContent.value,
isCompareMode.value,
testVariables
testVariables || {}
);
};