refactor(ui): 将导入导出功能抽离为独立组件
- 新增 ImportExportDialog 组件,专门处理导入导出功能 - 重构 ContextEditor,移除内联的导入导出对话框代码 - 支持 4 种导入格式 (smart/openai/langfuse/conversation) - 支持 2 种导出格式 (standard/openai) - 更新相关单元测试和 E2E 测试 - 同步更新三语言国际化文件 优化: - 代码量减少约 586 行 (24%) - 组件职责更清晰,符合单一职责原则 - 提高代码可维护性和复用性
This commit is contained in:
1 parent
dee32ae979
commit
3bc03c3be5
8 files changed
+934
-713
No files matched your search
@@ -1265,239 +1265,23 @@
|
||||
</NModal>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<NModal
|
||||
v-model:show="showImportDialog"
|
||||
preset="dialog"
|
||||
:title="t('contextEditor.importTitle')"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 格式选择 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.importFormat")
|
||||
}}</label>
|
||||
<NSpace size="small" wrap>
|
||||
<NButton
|
||||
v-for="format in importFormats"
|
||||
:key="format.id"
|
||||
@click="selectedImportFormat = format.id"
|
||||
:type="
|
||||
selectedImportFormat === format.id
|
||||
? 'primary'
|
||||
: 'default'
|
||||
"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ format.name }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{
|
||||
importFormats.find((f) => f.id === selectedImportFormat)
|
||||
?.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传 -->
|
||||
<div class="mb-4">
|
||||
<NSpace align="center" :size="8" class="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInputRef"
|
||||
accept=".json,.txt"
|
||||
@change="handleFileUpload"
|
||||
class="hidden"
|
||||
/>
|
||||
<NButton
|
||||
@click="fileInputRef?.click()"
|
||||
secondary
|
||||
:size="buttonSize"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.selectFile") }}
|
||||
</NButton>
|
||||
<NText depth="3" class="text-sm">
|
||||
{{ t("contextEditor.orPasteText") }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<!-- 文本输入区域 -->
|
||||
<NInput
|
||||
v-model:value="importData"
|
||||
type="textarea"
|
||||
:placeholder="getImportPlaceholder()"
|
||||
:autosize="{ minRows: 12, maxRows: 16 }"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
|
||||
<div v-if="importError" class="text-sm text-red-500 mt-2">
|
||||
{{ importError }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="showImportDialog = false" :size="buttonSize">
|
||||
{{ t("common.cancel") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
@click="handleImportSubmit"
|
||||
:disabled="!importData.trim()"
|
||||
type="primary"
|
||||
:size="buttonSize"
|
||||
:loading="contextEditor.isLoading.value"
|
||||
>
|
||||
{{ t("contextEditor.import") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
<ImportExportDialog
|
||||
v-model:visible="showImportDialog"
|
||||
mode="import"
|
||||
:messages="[]"
|
||||
@import-success="handleImportSuccess"
|
||||
@export-error="handleExportError"
|
||||
/>
|
||||
|
||||
<!-- 导出对话框 -->
|
||||
<NModal
|
||||
v-model:show="showExportDialog"
|
||||
preset="dialog"
|
||||
:title="t('contextEditor.exportTitle')"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 格式选择 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.exportFormat")
|
||||
}}</label>
|
||||
<NSpace size="small" wrap>
|
||||
<NButton
|
||||
v-for="format in exportFormats"
|
||||
:key="format.id"
|
||||
@click="selectedExportFormat = format.id"
|
||||
:type="
|
||||
selectedExportFormat === format.id
|
||||
? 'primary'
|
||||
: 'default'
|
||||
"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ format.name }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{
|
||||
exportFormats.find((f) => f.id === selectedExportFormat)
|
||||
?.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 导出预览 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.exportPreview")
|
||||
}}</label>
|
||||
<NInput
|
||||
:value="
|
||||
JSON.stringify(
|
||||
{
|
||||
messages: localState.messages,
|
||||
metadata: {
|
||||
variables: localState.variables,
|
||||
tools: localState.tools,
|
||||
exportTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)
|
||||
"
|
||||
readonly
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 8, maxRows: 12 }"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<NSpace justify="space-between">
|
||||
<NButton @click="showExportDialog = false" :size="buttonSize">
|
||||
{{ t("common.cancel") }}
|
||||
</NButton>
|
||||
|
||||
<NSpace>
|
||||
<NButton
|
||||
@click="handleExportToClipboard"
|
||||
secondary
|
||||
:size="buttonSize"
|
||||
:loading="contextEditor.isLoading.value"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.copyToClipboard") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
@click="handleExportToFile"
|
||||
type="primary"
|
||||
:size="buttonSize"
|
||||
:loading="contextEditor.isLoading.value"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.saveToFile") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
<ImportExportDialog
|
||||
v-model:visible="showExportDialog"
|
||||
mode="export"
|
||||
:messages="localState.messages"
|
||||
:tools="localState.tools"
|
||||
@export-success="handleExportSuccess"
|
||||
@export-error="handleExportError"
|
||||
/>
|
||||
|
||||
<!-- 变量编辑对话框 -->
|
||||
<NModal
|
||||
@@ -1656,8 +1440,8 @@ import { useResponsive } from "../../composables/ui/useResponsive";
|
||||
import { usePerformanceMonitor } from "../../composables/performance/usePerformanceMonitor";
|
||||
import { useDebounceThrottle } from '../../composables/performance/useDebounceThrottle';
|
||||
import { useAccessibility } from "../../composables/accessibility/useAccessibility";
|
||||
import { useContextEditor } from '../../composables/context/useContextEditor';
|
||||
import { useTemporaryVariables } from '../../composables/variable/useTemporaryVariables';
|
||||
import ImportExportDialog from './ImportExportDialog.vue';
|
||||
import { useAggregatedVariables } from '../../composables/variable/useAggregatedVariables';
|
||||
import {
|
||||
quickTemplateManager,
|
||||
@@ -1672,7 +1456,6 @@ import type {
|
||||
ConversationMessage,
|
||||
ToolDefinition,
|
||||
} from "@prompt-optimizer/core";
|
||||
import type { StandardPromptData } from "../../types";
|
||||
import {
|
||||
PREDEFINED_VARIABLES,
|
||||
type PredefinedVariable,
|
||||
@@ -1716,9 +1499,6 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<ContextEditorEvents>();
|
||||
|
||||
// 导入导出功能
|
||||
const contextEditor = useContextEditor();
|
||||
|
||||
// 临时变量管理
|
||||
const tempVars = useTemporaryVariables();
|
||||
|
||||
@@ -1746,14 +1526,10 @@ const loading = ref(false);
|
||||
const activeTab = ref("messages");
|
||||
const localVisible = ref(props.visible);
|
||||
|
||||
// 导入导出状态
|
||||
// 导入导出对话框状态
|
||||
const showImportDialog = ref(false);
|
||||
const showExportDialog = ref(false);
|
||||
const importData = ref("");
|
||||
const importError = ref("");
|
||||
const selectedImportFormat = ref("smart");
|
||||
const selectedExportFormat = ref("standard");
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 变量值输入框引用(用于自动聚焦)
|
||||
const variableValueInputRef = ref(null);
|
||||
|
||||
@@ -1906,41 +1682,6 @@ const quickTemplates = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const normalizeMessage = (
|
||||
msg: Partial<ConversationMessage>,
|
||||
): ConversationMessage => {
|
||||
const normalizedRole = (msg.role ?? "user") as ConversationMessage["role"];
|
||||
|
||||
let content = "";
|
||||
if (typeof msg.content === "string") {
|
||||
content = msg.content;
|
||||
} else if (msg.content != null) {
|
||||
try {
|
||||
content =
|
||||
typeof msg.content === "object"
|
||||
? JSON.stringify(msg.content)
|
||||
: String(msg.content);
|
||||
} catch {
|
||||
content = String(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
const normalized: ConversationMessage = {
|
||||
role: normalizedRole,
|
||||
content,
|
||||
};
|
||||
|
||||
if (typeof msg.name === "string") normalized.name = msg.name;
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
normalized.tool_calls =
|
||||
msg.tool_calls as ConversationMessage["tool_calls"];
|
||||
}
|
||||
if (typeof msg.tool_call_id === "string")
|
||||
normalized.tool_call_id = msg.tool_call_id;
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// 工具函数(统一使用注入函数)
|
||||
const getMessageVariables = (content: string) => {
|
||||
const detected = props.scanVariables(content || "") || [];
|
||||
@@ -2527,266 +2268,32 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
// 导入导出方法
|
||||
const importFormats = computed(() => [
|
||||
{ id: "smart", name: t("contextEditor.importFormats.smart.name"), description: t("contextEditor.importFormats.smart.description") },
|
||||
{ id: "conversation", name: t("contextEditor.importFormats.conversation.name"), description: t("contextEditor.importFormats.conversation.description") },
|
||||
{ id: "openai", name: t("contextEditor.importFormats.openai.name"), description: t("contextEditor.importFormats.openai.description") },
|
||||
{ id: "langfuse", name: t("contextEditor.importFormats.langfuse.name"), description: t("contextEditor.importFormats.langfuse.description") },
|
||||
]);
|
||||
// 导入导出事件处理
|
||||
interface ImportSuccessData {
|
||||
messages: ConversationMessage[];
|
||||
tools?: ToolDefinition[];
|
||||
}
|
||||
|
||||
type ExportFormat = "standard" | "openai" | "template";
|
||||
const handleImportSuccess = (data: ImportSuccessData) => {
|
||||
// 将导入的数据同步到本地状态
|
||||
localState.value.messages = data.messages;
|
||||
localState.value.tools = data.tools || [];
|
||||
|
||||
const exportFormats = computed(() => [
|
||||
{
|
||||
id: "standard" as ExportFormat,
|
||||
name: t("contextEditor.exportFormats.standard.name"),
|
||||
description: t("contextEditor.exportFormats.standard.description"),
|
||||
},
|
||||
{
|
||||
id: "openai" as ExportFormat,
|
||||
name: t("contextEditor.exportFormats.openai.name"),
|
||||
description: t("contextEditor.exportFormats.openai.description"),
|
||||
},
|
||||
{
|
||||
id: "template" as ExportFormat,
|
||||
name: t("contextEditor.exportFormats.template.name"),
|
||||
description: t("contextEditor.exportFormats.template.description"),
|
||||
},
|
||||
]);
|
||||
handleStateChange();
|
||||
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
importError.value = "";
|
||||
|
||||
const success = await contextEditor.importFromFile(file);
|
||||
|
||||
if (success && contextEditor.currentData.value) {
|
||||
// 将导入的数据同步到本地状态
|
||||
const data = contextEditor.currentData.value;
|
||||
localState.value.messages = (data.messages || []).map((msg) =>
|
||||
normalizeMessage(msg),
|
||||
);
|
||||
// 不导入 variables,临时变量不持久化
|
||||
localState.value.tools = data.tools || [];
|
||||
|
||||
handleStateChange();
|
||||
showImportDialog.value = false;
|
||||
importData.value = "";
|
||||
importError.value = "";
|
||||
|
||||
// 切换到消息编辑标签页
|
||||
activeTab.value = "messages";
|
||||
announce(t("contextEditor.importSuccess"), "polite");
|
||||
} else {
|
||||
importError.value = t("contextEditor.importFailed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("File upload error:", err);
|
||||
const errorMsg = err instanceof Error ? err.message : t("contextEditor.importFailed");
|
||||
importError.value = errorMsg;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 切换到消息编辑标签页
|
||||
activeTab.value = "messages";
|
||||
announce(t("contextEditor.importSuccess"), "polite");
|
||||
};
|
||||
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importData.value.trim()) {
|
||||
importError.value = t("contextEditor.importDataRequired");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
importError.value = "";
|
||||
const jsonData = JSON.parse(importData.value);
|
||||
let result;
|
||||
|
||||
switch (selectedImportFormat.value) {
|
||||
case "smart":
|
||||
result = contextEditor.smartImport(jsonData);
|
||||
break;
|
||||
case "openai":
|
||||
result = contextEditor.convertFromOpenAI(jsonData);
|
||||
break;
|
||||
case "langfuse":
|
||||
result = contextEditor.convertFromLangFuse(jsonData);
|
||||
break;
|
||||
case "conversation":
|
||||
// 直接设置为对话格式
|
||||
if (Array.isArray(jsonData)) {
|
||||
localState.value.messages = jsonData.map(
|
||||
(msg: Partial<ConversationMessage>) =>
|
||||
normalizeMessage(msg),
|
||||
);
|
||||
} else if (
|
||||
jsonData.messages &&
|
||||
Array.isArray(jsonData.messages)
|
||||
) {
|
||||
localState.value.messages = jsonData.messages.map(
|
||||
(msg: Partial<ConversationMessage>) =>
|
||||
normalizeMessage(msg),
|
||||
);
|
||||
// 不导入 variables,临时变量不持久化
|
||||
localState.value.tools = jsonData.tools || [];
|
||||
} else {
|
||||
importError.value = t("contextEditor.invalidConversationFormat");
|
||||
return;
|
||||
}
|
||||
handleStateChange();
|
||||
showImportDialog.value = false;
|
||||
importData.value = "";
|
||||
importError.value = "";
|
||||
activeTab.value = "messages";
|
||||
announce(t("contextEditor.importSuccess"), "polite");
|
||||
return;
|
||||
default:
|
||||
importError.value = t("contextEditor.unsupportedImportFormat");
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理转换结果
|
||||
if (result && result.success && contextEditor.currentData.value) {
|
||||
// 将导入的数据同步到本地状态
|
||||
const data = contextEditor.currentData.value;
|
||||
localState.value.messages = (data.messages || []).map((msg) =>
|
||||
normalizeMessage(msg),
|
||||
);
|
||||
// 不导入 variables,临时变量不持久化
|
||||
localState.value.tools = data.tools || [];
|
||||
|
||||
handleStateChange();
|
||||
showImportDialog.value = false;
|
||||
importData.value = "";
|
||||
importError.value = "";
|
||||
activeTab.value = "messages";
|
||||
announce(t("contextEditor.importSuccess"), "polite");
|
||||
} else {
|
||||
importError.value = result?.error || t("contextEditor.importFailed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Import error:", err);
|
||||
const errorMsg =
|
||||
err instanceof Error ? err.message : t("contextEditor.invalidJsonFormat");
|
||||
importError.value = errorMsg;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const handleExportSuccess = () => {
|
||||
announce(t("contextEditor.exportSuccess"), "polite");
|
||||
};
|
||||
|
||||
const handleExportToFile = () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 准备导出数据 - 转换为 StandardPromptData 格式
|
||||
const exportData: StandardPromptData = {
|
||||
messages: localState.value.messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
// 保留其他可能的属性
|
||||
...(msg.name && { name: msg.name }),
|
||||
...(msg.tool_calls && { tool_calls: msg.tool_calls }),
|
||||
...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }),
|
||||
})),
|
||||
tools: localState.value.tools,
|
||||
metadata: {
|
||||
// 不导出临时变量(会话级别,不持久化)
|
||||
exportTime: new Date().toISOString(),
|
||||
version: "1.0",
|
||||
source: "manual",
|
||||
origin: "context_editor",
|
||||
},
|
||||
};
|
||||
|
||||
// 设置导出数据到contextEditor
|
||||
contextEditor.setData(exportData);
|
||||
|
||||
// 执行导出
|
||||
const success = contextEditor.exportToFile(
|
||||
selectedExportFormat.value as ExportFormat,
|
||||
`context-export-${Date.now()}`,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
showExportDialog.value = false;
|
||||
announce(t("contextEditor.exportSuccess"), "polite");
|
||||
} else {
|
||||
throw new Error(t("contextEditor.exportFailed"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Export to file error:", err);
|
||||
const errorMsg = err instanceof Error ? err.message : t("contextEditor.exportFailed");
|
||||
// TODO: 显示错误提示给用户
|
||||
announce(`${t("contextEditor.exportFailed")}: ${errorMsg}`, "assertive");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportToClipboard = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 准备导出数据 - 转换为 StandardPromptData 格式
|
||||
const exportData: StandardPromptData = {
|
||||
messages: localState.value.messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
// 保留其他可能的属性
|
||||
...(msg.name && { name: msg.name }),
|
||||
...(msg.tool_calls && { tool_calls: msg.tool_calls }),
|
||||
...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }),
|
||||
})),
|
||||
tools: localState.value.tools,
|
||||
metadata: {
|
||||
// 不导出临时变量(会话级别,不持久化)
|
||||
exportTime: new Date().toISOString(),
|
||||
version: "1.0",
|
||||
source: "manual",
|
||||
origin: "context_editor",
|
||||
},
|
||||
};
|
||||
|
||||
// 设置导出数据到contextEditor
|
||||
contextEditor.setData(exportData);
|
||||
|
||||
// 执行导出到剪贴板
|
||||
const success = await contextEditor.exportToClipboard(
|
||||
selectedExportFormat.value as ExportFormat,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
showExportDialog.value = false;
|
||||
announce(t("contextEditor.copySuccess"), "polite");
|
||||
} else {
|
||||
throw new Error(t("contextEditor.copyFailed"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Export to clipboard error:", err);
|
||||
const errorMsg = err instanceof Error ? err.message : t("contextEditor.exportFailed");
|
||||
// TODO: 显示错误提示给用户
|
||||
announce(`${t("contextEditor.copyFailed")}: ${errorMsg}`, "assertive");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getImportPlaceholder = () => {
|
||||
switch (selectedImportFormat.value) {
|
||||
case "openai":
|
||||
return t("contextEditor.importPlaceholders.openai");
|
||||
case "langfuse":
|
||||
return t("contextEditor.importPlaceholders.langfuse");
|
||||
case "conversation":
|
||||
return t("contextEditor.importPlaceholders.conversation");
|
||||
case "smart":
|
||||
default:
|
||||
return t("contextEditor.importPlaceholders.smart");
|
||||
}
|
||||
const handleExportError = (message?: string) => {
|
||||
const fallbackMessage = message || t("contextEditor.exportFailed");
|
||||
console.error(fallbackMessage);
|
||||
announce(fallbackMessage, "assertive");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
<template>
|
||||
<!-- 导入模式 -->
|
||||
<NModal
|
||||
v-if="mode === 'import'"
|
||||
v-model:show="localVisible"
|
||||
preset="dialog"
|
||||
:title="t('contextEditor.importTitle')"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
:mask-closable="false"
|
||||
@update:show="handleVisibilityChange"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 格式选择 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.importFormat")
|
||||
}}</label>
|
||||
<NSpace size="small" wrap>
|
||||
<NButton
|
||||
v-for="format in importFormats"
|
||||
:key="format.id"
|
||||
@click="selectedImportFormat = format.id"
|
||||
:type="
|
||||
selectedImportFormat === format.id
|
||||
? 'primary'
|
||||
: 'default'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ format.name }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{
|
||||
importFormats.find((f) => f.id === selectedImportFormat)
|
||||
?.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传 -->
|
||||
<div class="mb-4">
|
||||
<NSpace align="center" :size="8" class="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInputRef"
|
||||
accept=".json,.txt"
|
||||
@change="handleFileUpload"
|
||||
class="hidden"
|
||||
/>
|
||||
<NButton
|
||||
@click="fileInputRef?.click()"
|
||||
secondary
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.selectFile") }}
|
||||
</NButton>
|
||||
<NText depth="3" class="text-sm">
|
||||
{{ t("contextEditor.orPasteText") }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<!-- 文本输入区域 -->
|
||||
<NInput
|
||||
v-model:value="importData"
|
||||
type="textarea"
|
||||
:placeholder="getImportPlaceholder()"
|
||||
:autosize="{ minRows: 12, maxRows: 16 }"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
|
||||
<div v-if="importError" class="text-sm text-red-500 mt-2">
|
||||
{{ importError }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="handleClose" size="small">
|
||||
{{ t("common.cancel") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
@click="handleImportSubmit"
|
||||
:disabled="!importData.trim()"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ t("contextEditor.import") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
<!-- 导出模式 -->
|
||||
<NModal
|
||||
v-else
|
||||
v-model:show="localVisible"
|
||||
preset="dialog"
|
||||
:title="t('contextEditor.exportTitle')"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
:mask-closable="false"
|
||||
@update:show="handleVisibilityChange"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 格式选择 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.exportFormat")
|
||||
}}</label>
|
||||
<NSpace size="small" wrap>
|
||||
<NButton
|
||||
v-for="format in exportFormats"
|
||||
:key="format.id"
|
||||
@click="selectedExportFormat = format.id"
|
||||
:type="
|
||||
selectedExportFormat === format.id
|
||||
? 'primary'
|
||||
: 'default'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ format.name }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{
|
||||
exportFormats.find((f) => f.id === selectedExportFormat)
|
||||
?.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 导出预览 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">{{
|
||||
t("contextEditor.exportPreview")
|
||||
}}</label>
|
||||
<NInput
|
||||
:value="exportPreviewData"
|
||||
readonly
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 8, maxRows: 12 }"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<div v-if="exportError" class="text-sm text-red-500 mt-2">
|
||||
{{ exportError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<NSpace justify="space-between">
|
||||
<NButton @click="handleClose" size="small">
|
||||
{{ t("common.cancel") }}
|
||||
</NButton>
|
||||
|
||||
<NSpace>
|
||||
<NButton
|
||||
@click="handleExportToClipboard"
|
||||
secondary
|
||||
size="small"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.copyToClipboard") }}
|
||||
</NButton>
|
||||
<NButton
|
||||
@click="handleExportToFile"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t("contextEditor.saveToFile") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
NModal,
|
||||
NButton,
|
||||
NSpace,
|
||||
NInput,
|
||||
NText,
|
||||
} from "naive-ui";
|
||||
import { useContextEditor } from '../../composables/context/useContextEditor';
|
||||
import type { ConversationMessage, ToolDefinition } from "@prompt-optimizer/core";
|
||||
import type { StandardPromptData } from "../../types";
|
||||
|
||||
// 类型定义
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
mode: 'import' | 'export';
|
||||
messages: ConversationMessage[];
|
||||
tools?: ToolDefinition[];
|
||||
}
|
||||
|
||||
interface ImportSuccessData {
|
||||
messages: ConversationMessage[];
|
||||
tools?: ToolDefinition[];
|
||||
}
|
||||
|
||||
// Props 和 Events
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tools: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [visible: boolean];
|
||||
'import-success': [data: ImportSuccessData];
|
||||
'export-success': [];
|
||||
'export-error': [message?: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 复用 useContextEditor
|
||||
const contextEditor = useContextEditor();
|
||||
|
||||
// 本地状态
|
||||
const localVisible = ref(props.visible);
|
||||
const loading = ref(false);
|
||||
const importData = ref("");
|
||||
const importError = ref("");
|
||||
const exportError = ref("");
|
||||
const selectedImportFormat = ref("smart");
|
||||
const selectedExportFormat = ref("standard");
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 同步 visible 状态
|
||||
watch(() => props.visible, (newVisible) => {
|
||||
localVisible.value = newVisible;
|
||||
if (newVisible) {
|
||||
// 重置状态
|
||||
importData.value = "";
|
||||
importError.value = "";
|
||||
exportError.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// 导入格式选项
|
||||
const importFormats = computed(() => [
|
||||
{ id: "smart", name: t("contextEditor.importFormats.smart.name"), description: t("contextEditor.importFormats.smart.description") },
|
||||
{ id: "openai", name: t("contextEditor.importFormats.openai.name"), description: t("contextEditor.importFormats.openai.description") },
|
||||
{ id: "langfuse", name: t("contextEditor.importFormats.langfuse.name"), description: t("contextEditor.importFormats.langfuse.description") },
|
||||
{ id: "conversation", name: t("contextEditor.importFormats.conversation.name"), description: t("contextEditor.importFormats.conversation.description") },
|
||||
]);
|
||||
|
||||
// 导出格式选项
|
||||
type ExportFormat = "standard" | "openai";
|
||||
|
||||
const exportFormats = computed(() => [
|
||||
{
|
||||
id: "standard" as ExportFormat,
|
||||
name: t("contextEditor.exportFormats.standard.name"),
|
||||
description: t("contextEditor.exportFormats.standard.description"),
|
||||
},
|
||||
{
|
||||
id: "openai" as ExportFormat,
|
||||
name: t("contextEditor.exportFormats.openai.name"),
|
||||
description: t("contextEditor.exportFormats.openai.description"),
|
||||
},
|
||||
]);
|
||||
|
||||
const IMPORT_JSON_SNIPPETS: Record<string, string> = {
|
||||
openai: `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Explain quantum entanglement."
|
||||
}
|
||||
],
|
||||
"model": "gpt-4o"
|
||||
}`,
|
||||
langfuse: `{
|
||||
"input": {
|
||||
"messages": [
|
||||
{"role": "system", "content": "system prompt"},
|
||||
{"role": "user", "content": "user prompt"}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"traceId": "abc123"
|
||||
}
|
||||
}`,
|
||||
conversation: `{
|
||||
"messages": [
|
||||
{"role": "system", "content": "System message"},
|
||||
{"role": "user", "content": "User request"}
|
||||
],
|
||||
"tools": []
|
||||
}`,
|
||||
smart: ``,
|
||||
};
|
||||
|
||||
// 导出预览数据
|
||||
const buildExportPayload = () => ({
|
||||
messages: props.messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
...(msg.name && { name: msg.name }),
|
||||
...(msg.tool_calls && { tool_calls: msg.tool_calls }),
|
||||
...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }),
|
||||
})),
|
||||
tools: props.tools,
|
||||
metadata: {
|
||||
exportTime: new Date().toISOString(),
|
||||
version: "1.0",
|
||||
source: "manual",
|
||||
origin: "import_export_dialog",
|
||||
},
|
||||
});
|
||||
|
||||
const exportPreviewData = computed(() => {
|
||||
const basePayload = buildExportPayload();
|
||||
if (selectedExportFormat.value === "openai") {
|
||||
const converter = contextEditor.services?.converter;
|
||||
if (converter) {
|
||||
const result = converter.toOpenAI(basePayload);
|
||||
if (result.success && result.data) {
|
||||
return JSON.stringify(result.data, null, 2);
|
||||
}
|
||||
console.error(
|
||||
result.error || "Failed to build OpenAI preview payload",
|
||||
);
|
||||
}
|
||||
}
|
||||
return JSON.stringify(basePayload, null, 2);
|
||||
});
|
||||
|
||||
// 事件处理
|
||||
const handleVisibilityChange = (visible: boolean) => {
|
||||
localVisible.value = visible;
|
||||
if (!visible) {
|
||||
importError.value = "";
|
||||
exportError.value = "";
|
||||
}
|
||||
emit('update:visible', visible);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handleVisibilityChange(false);
|
||||
};
|
||||
|
||||
// 消息规范化
|
||||
const normalizeMessage = (
|
||||
msg: Partial<ConversationMessage>,
|
||||
): ConversationMessage => {
|
||||
const normalizedRole = (msg.role ?? "user") as ConversationMessage["role"];
|
||||
|
||||
let content = "";
|
||||
if (typeof msg.content === "string") {
|
||||
content = msg.content;
|
||||
} else if (msg.content != null) {
|
||||
try {
|
||||
content =
|
||||
typeof msg.content === "object"
|
||||
? JSON.stringify(msg.content)
|
||||
: String(msg.content);
|
||||
} catch {
|
||||
content = String(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
const normalized: ConversationMessage = {
|
||||
role: normalizedRole,
|
||||
content,
|
||||
};
|
||||
|
||||
if (typeof msg.name === "string") normalized.name = msg.name;
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
normalized.tool_calls =
|
||||
msg.tool_calls as ConversationMessage["tool_calls"];
|
||||
}
|
||||
if (typeof msg.tool_call_id === "string")
|
||||
normalized.tool_call_id = msg.tool_call_id;
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// 文件上传处理
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
importError.value = "";
|
||||
|
||||
const success = await contextEditor.importFromFile(file);
|
||||
|
||||
if (success && contextEditor.currentData.value) {
|
||||
const data = contextEditor.currentData.value;
|
||||
emit('import-success', {
|
||||
messages: (data.messages || []).map((msg) => normalizeMessage(msg)),
|
||||
tools: data.tools || [],
|
||||
});
|
||||
handleClose();
|
||||
} else {
|
||||
importError.value = t("contextEditor.importFailed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("File upload error:", err);
|
||||
const errorMsg = err instanceof Error ? err.message : t("contextEditor.importFailed");
|
||||
importError.value = errorMsg;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导入提交处理
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importData.value.trim()) {
|
||||
importError.value = t("contextEditor.importDataRequired");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
importError.value = "";
|
||||
const jsonData = JSON.parse(importData.value);
|
||||
let result;
|
||||
|
||||
switch (selectedImportFormat.value) {
|
||||
case "smart":
|
||||
result = contextEditor.smartImport(jsonData);
|
||||
break;
|
||||
case "openai":
|
||||
result = contextEditor.convertFromOpenAI(jsonData);
|
||||
break;
|
||||
case "langfuse":
|
||||
result = contextEditor.convertFromLangFuse(jsonData);
|
||||
break;
|
||||
case "conversation":
|
||||
// 直接设置为对话格式
|
||||
if (Array.isArray(jsonData)) {
|
||||
emit('import-success', {
|
||||
messages: jsonData.map((msg: Partial<ConversationMessage>) => normalizeMessage(msg)),
|
||||
});
|
||||
} else if (jsonData.messages && Array.isArray(jsonData.messages)) {
|
||||
emit('import-success', {
|
||||
messages: jsonData.messages.map((msg: Partial<ConversationMessage>) => normalizeMessage(msg)),
|
||||
tools: jsonData.tools || [],
|
||||
});
|
||||
} else {
|
||||
importError.value = t("contextEditor.invalidConversationFormat");
|
||||
return;
|
||||
}
|
||||
importError.value = "";
|
||||
handleClose();
|
||||
return;
|
||||
default:
|
||||
importError.value = t("contextEditor.unsupportedImportFormat");
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理转换结果
|
||||
if (result && result.success && contextEditor.currentData.value) {
|
||||
const data = contextEditor.currentData.value;
|
||||
emit('import-success', {
|
||||
messages: (data.messages || []).map((msg) => normalizeMessage(msg)),
|
||||
tools: data.tools || [],
|
||||
});
|
||||
importError.value = "";
|
||||
handleClose();
|
||||
} else {
|
||||
importError.value = result?.error || t("contextEditor.importFailed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Import error:", err);
|
||||
const errorMsg = err instanceof Error ? err.message : t("contextEditor.invalidJsonFormat");
|
||||
importError.value = errorMsg;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导出到文件
|
||||
const notifyExportError = (key: string, err?: unknown) => {
|
||||
const baseMessage = t(key);
|
||||
const details = err instanceof Error ? err.message : "";
|
||||
const composed = details ? `${baseMessage}: ${details}` : baseMessage;
|
||||
exportError.value = composed;
|
||||
emit('export-error', composed);
|
||||
console.error(composed);
|
||||
};
|
||||
|
||||
const handleExportToFile = () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
exportError.value = "";
|
||||
|
||||
const exportData: StandardPromptData = buildExportPayload();
|
||||
contextEditor.setData(exportData);
|
||||
|
||||
const success = contextEditor.exportToFile(
|
||||
selectedExportFormat.value as ExportFormat,
|
||||
`context-export-${Date.now()}`,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit('export-success');
|
||||
handleClose();
|
||||
} else {
|
||||
throw new Error(t("contextEditor.exportFailed"));
|
||||
}
|
||||
} catch (err) {
|
||||
notifyExportError("contextEditor.exportFailed", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导出到剪贴板
|
||||
const handleExportToClipboard = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
exportError.value = "";
|
||||
|
||||
const exportData: StandardPromptData = buildExportPayload();
|
||||
contextEditor.setData(exportData);
|
||||
|
||||
const success = await contextEditor.exportToClipboard(
|
||||
selectedExportFormat.value as ExportFormat,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit('export-success');
|
||||
handleClose();
|
||||
} else {
|
||||
throw new Error(t("contextEditor.copyFailed"));
|
||||
}
|
||||
} catch (err) {
|
||||
notifyExportError("contextEditor.copyFailed", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取导入占位符
|
||||
const getImportPlaceholder = () => {
|
||||
const format = selectedImportFormat.value;
|
||||
const prefix = t(`contextEditor.importPlaceholders.${format}`);
|
||||
const snippet = IMPORT_JSON_SNIPPETS[format] || IMPORT_JSON_SNIPPETS.smart;
|
||||
return snippet ? `${prefix}\n${snippet}` : prefix;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1654,22 +1654,21 @@ export default {
|
||||
// Import/Export formats
|
||||
importFormats: {
|
||||
smart: { name: "Smart Detection", description: "Auto-detect format and convert" },
|
||||
conversation: { name: "Conversation", description: "Standard conversation message format" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API request format" },
|
||||
langfuse: { name: "LangFuse", description: "LangFuse tracking data format" },
|
||||
conversation: { name: "Internal Format", description: "Prompt Optimizer internal JSON structure" },
|
||||
},
|
||||
exportFormats: {
|
||||
standard: { name: "Standard", description: "Internal standard data format" },
|
||||
standard: { name: "Internal Format", description: "Prompt Optimizer internal standard format" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API compatible format" },
|
||||
template: { name: "Template", description: "Reusable template format" },
|
||||
},
|
||||
|
||||
// Import placeholders
|
||||
// Import placeholders (localized text, JSON snippet fixed in component)
|
||||
importPlaceholders: {
|
||||
openai: 'OpenAI API request format, e.g.:\n{\n "messages": [...],\n "model": "gpt-4"\n}',
|
||||
langfuse: 'LangFuse tracking data, e.g.:\n{\n "input": {\n "messages": [...]\n }\n}',
|
||||
conversation: 'Standard conversation format, e.g.:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}',
|
||||
smart: "Paste any supported JSON format, system will auto-detect",
|
||||
openai: "OpenAI API request format (example below):",
|
||||
langfuse: "LangFuse trace payload (example below):",
|
||||
conversation: "Standard conversation JSON (example below):",
|
||||
smart: "Paste any supported JSON format (OpenAI, LangFuse, or conversation array). The editor will auto-detect the format.",
|
||||
},
|
||||
|
||||
// Console errors (developer logs)
|
||||
|
||||
@@ -1595,22 +1595,21 @@ export default {
|
||||
// Import/Export formats
|
||||
importFormats: {
|
||||
smart: { name: "智能识别", description: "自动检测格式并转换" },
|
||||
conversation: { name: "会话格式", description: "标准的会话消息格式" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API 请求格式" },
|
||||
langfuse: { name: "LangFuse", description: "LangFuse 追踪数据格式" },
|
||||
conversation: { name: "内部格式", description: "Prompt Optimizer 内部标准 JSON 结构" },
|
||||
},
|
||||
exportFormats: {
|
||||
standard: { name: "标准格式", description: "内部标准数据格式" },
|
||||
standard: { name: "内部格式", description: "Prompt Optimizer 内部标准数据格式" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API 兼容格式" },
|
||||
template: { name: "模板格式", description: "可复用的模板格式" },
|
||||
},
|
||||
|
||||
// Import placeholders
|
||||
// Import placeholders (文本可本地化,JSON 示例固定为英文)
|
||||
importPlaceholders: {
|
||||
openai: 'OpenAI API 请求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}',
|
||||
langfuse: 'LangFuse 追踪数据,例如:\n{\n "input": {\n "messages": [...]\n }\n}',
|
||||
conversation: '标准会话格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}',
|
||||
smart: "粘贴任意支持格式的 JSON 数据,系统将自动识别",
|
||||
openai: "OpenAI API 请求格式(示例见下方):",
|
||||
langfuse: "LangFuse 追踪数据格式(示例见下方):",
|
||||
conversation: "标准会话 JSON 格式(示例见下方):",
|
||||
smart: "粘贴任意受支持的 JSON(OpenAI、LangFuse 或会话数组),系统会自动识别格式。",
|
||||
},
|
||||
|
||||
// Console errors (开发者日志)
|
||||
|
||||
@@ -1559,22 +1559,21 @@ export default {
|
||||
// Import/Export formats
|
||||
importFormats: {
|
||||
smart: { name: "智慧識別", description: "自動偵測格式並轉換" },
|
||||
conversation: { name: "會話格式", description: "標準的會話訊息格式" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API 請求格式" },
|
||||
langfuse: { name: "LangFuse", description: "LangFuse 追蹤資料格式" },
|
||||
conversation: { name: "內部格式", description: "Prompt Optimizer 內部標準 JSON 結構" },
|
||||
},
|
||||
exportFormats: {
|
||||
standard: { name: "標準格式", description: "內部標準資料格式" },
|
||||
standard: { name: "內部格式", description: "Prompt Optimizer 內部標準資料格式" },
|
||||
openai: { name: "OpenAI", description: "OpenAI API 相容格式" },
|
||||
template: { name: "範本格式", description: "可複用的範本格式" },
|
||||
},
|
||||
|
||||
// Import placeholders
|
||||
// Import placeholders (文字本地化,JSON 範例固定英文)
|
||||
importPlaceholders: {
|
||||
openai: 'OpenAI API 請求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}',
|
||||
langfuse: 'LangFuse 追蹤資料,例如:\n{\n "input": {\n "messages": [...]\n }\n}',
|
||||
conversation: '標準會話格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}',
|
||||
smart: "貼上任意支援格式的 JSON 資料,系統將自動識別",
|
||||
openai: "OpenAI API 請求格式(下方示例):",
|
||||
langfuse: "LangFuse 追蹤資料格式(下方示例):",
|
||||
conversation: "標準會話 JSON 格式(下方示例):",
|
||||
smart: "貼上任意支援的 JSON(OpenAI、LangFuse 或會話陣列),系統會自動辨識格式。",
|
||||
},
|
||||
|
||||
// Console errors (開發者日誌)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nextTick } from 'vue'
|
||||
// 组件导入
|
||||
import ConversationManager from '../../src/components/context-mode/ConversationManager.vue'
|
||||
import ContextEditor from '../../src/components/context-mode/ContextEditor.vue'
|
||||
import ImportExportDialog from '../../src/components/context-mode/ImportExportDialog.vue'
|
||||
import VariableManagerModal from '../../src/components/variable/VariableManagerModal.vue'
|
||||
|
||||
// Mock Naive UI 组件 - 简化版本用于E2E测试
|
||||
@@ -214,7 +215,18 @@ vi.mock('../../src/composables/useAccessibility', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../src/composables/useContextEditor', () => ({
|
||||
const mockConverter = {
|
||||
toOpenAI: vi.fn().mockImplementation((data) => ({
|
||||
success: true,
|
||||
data: {
|
||||
model: data.model || 'gpt-test',
|
||||
messages: data.messages,
|
||||
tools: data.tools
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
vi.mock('../../src/composables/context/useContextEditor', () => ({
|
||||
useContextEditor: () => ({
|
||||
currentData: { value: null },
|
||||
isLoading: { value: false },
|
||||
@@ -222,9 +234,12 @@ vi.mock('../../src/composables/useContextEditor', () => ({
|
||||
convertFromOpenAI: vi.fn(),
|
||||
convertFromLangFuse: vi.fn(),
|
||||
importFromFile: vi.fn(),
|
||||
exportToFile: vi.fn().mockResolvedValue(true),
|
||||
exportToFile: vi.fn().mockReturnValue(true),
|
||||
exportToClipboard: vi.fn().mockResolvedValue(true),
|
||||
setData: vi.fn()
|
||||
setData: vi.fn(),
|
||||
services: {
|
||||
converter: mockConverter
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -509,21 +524,12 @@ describe('完整用户流程E2E测试', () => {
|
||||
|
||||
describe('3. 导入导出和格式转换功能', () => {
|
||||
it('应该支持多种格式的导入功能', async () => {
|
||||
contextEditorWrapper = mount(ContextEditor, {
|
||||
const dialogWrapper = mount(ImportExportDialog, {
|
||||
props: {
|
||||
visible: true,
|
||||
state: {
|
||||
messages: [],
|
||||
variables: {},
|
||||
tools: [],
|
||||
showVariablePreview: true,
|
||||
showToolManager: false,
|
||||
mode: 'edit'
|
||||
},
|
||||
scanVariables,
|
||||
replaceVariables,
|
||||
isPredefinedVariable: () => false,
|
||||
variableManager: createMockVariableManager()
|
||||
mode: 'import',
|
||||
messages: [],
|
||||
tools: []
|
||||
},
|
||||
global: {
|
||||
stubs: {},
|
||||
@@ -535,40 +541,48 @@ describe('完整用户流程E2E测试', () => {
|
||||
|
||||
await nextTick()
|
||||
|
||||
// 验证导入功能存在
|
||||
expect(contextEditorWrapper.vm.handleImport).toBeDefined()
|
||||
expect(contextEditorWrapper.vm.handleImportSubmit).toBeDefined()
|
||||
|
||||
// 验证支持的导入格式
|
||||
const importFormats = contextEditorWrapper.vm.importFormats
|
||||
expect(importFormats).toEqual([
|
||||
{ id: 'smart', name: '智能识别', description: '自动检测格式并转换' },
|
||||
{ id: 'conversation', name: '会话格式', description: '标准的会话消息格式' },
|
||||
{ id: 'openai', name: 'OpenAI', description: 'OpenAI API 请求格式' },
|
||||
{ id: 'langfuse', name: 'LangFuse', description: 'LangFuse 追踪数据格式' }
|
||||
])
|
||||
const formats = dialogWrapper.vm.importFormats
|
||||
expect(formats.map(f => f.id)).toEqual(['smart', 'openai', 'langfuse', 'conversation'])
|
||||
|
||||
// 测试对话格式导入
|
||||
const conversationData = {
|
||||
dialogWrapper.vm.importData = JSON.stringify({
|
||||
messages: [
|
||||
{ role: 'system', content: '测试系统消息' },
|
||||
{ role: 'user', content: '测试用户消息' }
|
||||
{ role: 'system', content: '系统消息' },
|
||||
{ role: 'user', content: '用户消息' }
|
||||
],
|
||||
variables: { testVar: 'testValue' }
|
||||
}
|
||||
tools: []
|
||||
})
|
||||
dialogWrapper.vm.selectedImportFormat = 'conversation'
|
||||
|
||||
// 设置导入数据
|
||||
contextEditorWrapper.vm.importData = JSON.stringify(conversationData)
|
||||
contextEditorWrapper.vm.selectedImportFormat = 'conversation'
|
||||
|
||||
// 执行导入
|
||||
await contextEditorWrapper.vm.handleImportSubmit()
|
||||
await nextTick()
|
||||
await dialogWrapper.vm.handleImportSubmit()
|
||||
|
||||
// 验证导入结果
|
||||
expect(contextEditorWrapper.emitted('update:state')).toBeTruthy()
|
||||
expect(dialogWrapper.emitted('import-success')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('应该支持多种格式的导出功能', async () => {
|
||||
const dialogWrapper = mount(ImportExportDialog, {
|
||||
props: {
|
||||
visible: true,
|
||||
mode: 'export',
|
||||
messages: [{ role: 'user', content: '测试消息' }],
|
||||
tools: []
|
||||
},
|
||||
global: {
|
||||
stubs: {},
|
||||
mocks: {
|
||||
announcements: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const formats = dialogWrapper.vm.exportFormats
|
||||
expect(formats.map(f => f.id)).toEqual(['standard', 'openai'])
|
||||
|
||||
await dialogWrapper.vm.handleExportToFile()
|
||||
|
||||
expect(dialogWrapper.emitted('export-success')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('4. 变量管理的跨组件协作', () => {
|
||||
@@ -887,10 +901,11 @@ describe('完整用户流程E2E测试', () => {
|
||||
// 验证最终状态(简化验证)
|
||||
expect(contextEditorWrapper.exists()).toBe(true)
|
||||
expect(contextEditorWrapper.vm).toBeTruthy()
|
||||
|
||||
// 验证导出功能可用
|
||||
expect(contextEditorWrapper.vm.handleExportToClipboard).toBeDefined()
|
||||
expect(contextEditorWrapper.vm.handleExportToFile).toBeDefined()
|
||||
|
||||
// 注意:导出功能已移至 ImportExportDialog 组件
|
||||
// 验证导入导出对话框可以打开
|
||||
expect(contextEditorWrapper.vm.handleImport).toBeDefined()
|
||||
expect(contextEditorWrapper.vm.handleExport).toBeDefined()
|
||||
})
|
||||
|
||||
it('应该正确处理错误和边界情况', async () => {
|
||||
|
||||
@@ -145,9 +145,9 @@ vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
const translations = {
|
||||
'contextEditor.importPlaceholders.openai': 'OpenAI API 请求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}',
|
||||
'contextEditor.importPlaceholders.langfuse': 'LangFuse 追踪数据,例如:\n{\n "input": {\n "messages": [...]\n }\n}',
|
||||
'contextEditor.importPlaceholders.conversation': '标准会话格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}',
|
||||
'contextEditor.importPlaceholders.openai': 'OpenAI API 请求格式(示例如下):',
|
||||
'contextEditor.importPlaceholders.langfuse': 'LangFuse 追踪数据格式(示例如下):',
|
||||
'contextEditor.importPlaceholders.conversation': '标准会话 JSON 格式(示例如下):',
|
||||
'contextEditor.importPlaceholders.smart': "粘贴任意支持格式的 JSON 数据,系统将自动识别"
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ describe('ContextEditor 综合测试', () => {
|
||||
|
||||
it('点击导入按钮应该打开导入对话框', async () => {
|
||||
wrapper = await createWrapper()
|
||||
|
||||
|
||||
await wrapper.vm.handleImport()
|
||||
expect(wrapper.vm.showImportDialog).toBe(true)
|
||||
})
|
||||
@@ -467,89 +467,12 @@ describe('ContextEditor 综合测试', () => {
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
await wrapper.vm.handleExport()
|
||||
expect(wrapper.vm.showExportDialog).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
it('导入对话框应该显示正确的格式选项', async () => {
|
||||
wrapper = await createWrapper()
|
||||
wrapper.vm.showImportDialog = true
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.importFormats).toHaveLength(4)
|
||||
expect(wrapper.vm.importFormats.map(f => f.id)).toEqual(['smart', 'conversation', 'openai', 'langfuse'])
|
||||
})
|
||||
|
||||
it('导出对话框应该显示正确的格式选项', async () => {
|
||||
wrapper = await createWrapper()
|
||||
wrapper.vm.showExportDialog = true
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.exportFormats).toHaveLength(3)
|
||||
expect(wrapper.vm.exportFormats.map(f => f.id)).toEqual(['standard', 'openai', 'template'])
|
||||
})
|
||||
|
||||
it('应该正确处理不同导入格式的占位符', async () => {
|
||||
wrapper = await createWrapper()
|
||||
|
||||
wrapper.vm.selectedImportFormat = 'openai'
|
||||
expect(wrapper.vm.getImportPlaceholder()).toContain('OpenAI API')
|
||||
|
||||
wrapper.vm.selectedImportFormat = 'langfuse'
|
||||
expect(wrapper.vm.getImportPlaceholder()).toContain('LangFuse')
|
||||
|
||||
wrapper.vm.selectedImportFormat = 'conversation'
|
||||
expect(wrapper.vm.getImportPlaceholder()).toContain('会话格式')
|
||||
|
||||
wrapper.vm.selectedImportFormat = 'smart'
|
||||
expect(wrapper.vm.getImportPlaceholder()).toContain('自动识别')
|
||||
})
|
||||
|
||||
it('导入失败时应该显示错误信息', async () => {
|
||||
mockContextEditor.smartImport.mockReturnValueOnce({
|
||||
success: false,
|
||||
error: '导入数据格式错误'
|
||||
})
|
||||
|
||||
wrapper = await createWrapper()
|
||||
wrapper.vm.selectedImportFormat = 'smart'
|
||||
wrapper.vm.importData = 'invalid json'
|
||||
|
||||
await wrapper.vm.handleImportSubmit()
|
||||
|
||||
expect(wrapper.vm.importError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('导出失败时应该处理错误', async () => {
|
||||
mockContextEditor.exportToFile.mockReturnValueOnce(false)
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
wrapper = await createWrapper({
|
||||
state: {
|
||||
...defaultProps.state,
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.handleExportToFile()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('导入成功后应该清空导入状态', async () => {
|
||||
wrapper = await createWrapper()
|
||||
wrapper.vm.selectedImportFormat = 'conversation'
|
||||
wrapper.vm.importData = '{"messages":[{"role":"user","content":"test"}]}'
|
||||
|
||||
await wrapper.vm.handleImportSubmit()
|
||||
|
||||
expect(wrapper.vm.showImportDialog).toBe(false)
|
||||
expect(wrapper.vm.importData).toBe('')
|
||||
expect(wrapper.vm.importError).toBe('')
|
||||
})
|
||||
// 注意:导入导出的详细功能测试已移至 ImportExportDialog.spec.ts
|
||||
})
|
||||
|
||||
describe('optimizationMode 参数传递', () => {
|
||||
@@ -698,36 +621,8 @@ describe('ContextEditor 综合测试', () => {
|
||||
expect(wrapper.emitted('update:visible')[0]).toEqual([false])
|
||||
})
|
||||
})
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('导入失败时应该显示错误信息', async () => {
|
||||
// 这个测试实际走的是JSON.parse失败的分支,测试输入无效JSON的错误处理
|
||||
wrapper = await createWrapper()
|
||||
wrapper.vm.selectedImportFormat = 'smart'
|
||||
wrapper.vm.importData = 'invalid json' // 无效JSON,触发JSON.parse失败
|
||||
|
||||
await wrapper.vm.handleImportSubmit()
|
||||
|
||||
// 根据组件代码第1262行:'数据格式错误,请检查JSON格式'
|
||||
// 但实际错误信息来自JSON.parse的SyntaxError message
|
||||
expect(wrapper.vm.importError).toContain('Unexpected token')
|
||||
})
|
||||
|
||||
it('文件上传失败时应该设置错误状态', async () => {
|
||||
mockContextEditor.importFromFile.mockResolvedValueOnce(false)
|
||||
|
||||
wrapper = await createWrapper()
|
||||
|
||||
const file = new File(['invalid content'], 'test.json', { type: 'application/json' })
|
||||
const event = { target: { files: [file] } }
|
||||
|
||||
await wrapper.vm.handleFileUpload(event)
|
||||
|
||||
expect(wrapper.vm.importError).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 导出类型以供其他测试使用
|
||||
export type MockContextEditor = typeof mockContextEditor
|
||||
export { mockQuickTemplates }
|
||||
export { mockQuickTemplates }
|
||||
@@ -0,0 +1,194 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import ImportExportDialog from '../../../src/components/context-mode/ImportExportDialog.vue'
|
||||
|
||||
const mockConverter = {
|
||||
toOpenAI: vi.fn()
|
||||
}
|
||||
|
||||
const mockContextEditor = {
|
||||
currentData: { value: null },
|
||||
isLoading: { value: false },
|
||||
smartImport: vi.fn(),
|
||||
convertFromOpenAI: vi.fn(),
|
||||
convertFromLangFuse: vi.fn(),
|
||||
importFromFile: vi.fn(),
|
||||
exportToFile: vi.fn(),
|
||||
exportToClipboard: vi.fn(),
|
||||
setData: vi.fn(),
|
||||
services: {
|
||||
converter: mockConverter
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/composables/context/useContextEditor', () => ({
|
||||
useContextEditor: () => mockContextEditor
|
||||
}))
|
||||
|
||||
const naiveStubs = {
|
||||
NModal: {
|
||||
name: 'NModal',
|
||||
template: '<div v-if="show"><slot /><slot name="action" /></div>',
|
||||
props: ['show', 'title', 'preset', 'style'],
|
||||
emits: ['update:show']
|
||||
},
|
||||
NButton: {
|
||||
name: 'NButton',
|
||||
template: '<button @click="$emit(\'click\')" :disabled="disabled"><slot /></button>',
|
||||
props: ['type', 'disabled', 'loading', 'size', 'secondary'],
|
||||
emits: ['click']
|
||||
},
|
||||
NSpace: {
|
||||
name: 'NSpace',
|
||||
template: '<div class="n-space"><slot /></div>',
|
||||
props: ['size', 'align', 'justify', 'wrap']
|
||||
},
|
||||
NInput: {
|
||||
name: 'NInput',
|
||||
template: '<textarea v-if="type === \'textarea\'" :value="value" @input="$emit(\'update:value\', $event.target.value)"></textarea>',
|
||||
props: ['value', 'type', 'placeholder', 'autosize', 'readonly'],
|
||||
emits: ['update:value']
|
||||
},
|
||||
NText: {
|
||||
name: 'NText',
|
||||
template: '<span><slot /></span>',
|
||||
props: ['depth', 'type']
|
||||
}
|
||||
}
|
||||
|
||||
type DialogProps = {
|
||||
visible: boolean;
|
||||
mode: 'import' | 'export';
|
||||
messages: any[];
|
||||
tools?: any[];
|
||||
}
|
||||
|
||||
const createWrapper = (props?: Partial<DialogProps>) => {
|
||||
return mount(ImportExportDialog, {
|
||||
props: {
|
||||
visible: true,
|
||||
mode: 'import',
|
||||
messages: [],
|
||||
tools: [],
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
stubs: naiveStubs
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('ImportExportDialog', () => {
|
||||
beforeEach(() => {
|
||||
mockContextEditor.currentData.value = null
|
||||
mockContextEditor.smartImport.mockReset()
|
||||
mockContextEditor.convertFromOpenAI.mockReset()
|
||||
mockContextEditor.convertFromLangFuse.mockReset()
|
||||
mockContextEditor.importFromFile.mockReset()
|
||||
mockContextEditor.exportToFile.mockReset()
|
||||
mockContextEditor.exportToClipboard.mockReset()
|
||||
mockContextEditor.setData.mockReset()
|
||||
mockContextEditor.exportToFile.mockReturnValue(true)
|
||||
mockContextEditor.exportToClipboard.mockResolvedValue(true)
|
||||
mockConverter.toOpenAI.mockReset()
|
||||
mockConverter.toOpenAI.mockImplementation((data) => ({
|
||||
success: true,
|
||||
data: {
|
||||
model: data.model || 'gpt-test',
|
||||
messages: data.messages,
|
||||
tools: data.tools
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
it('应该提供完整的导入格式选项', () => {
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.vm.importFormats.map(f => f.id)).toEqual(['smart', 'openai', 'langfuse', 'conversation'])
|
||||
})
|
||||
|
||||
it('会话格式导入成功后应该触发 import-success 事件', async () => {
|
||||
const wrapper = createWrapper()
|
||||
wrapper.vm.selectedImportFormat = 'conversation'
|
||||
wrapper.vm.importData = JSON.stringify({
|
||||
messages: [{ role: 'user', content: 'hello' }]
|
||||
})
|
||||
|
||||
await wrapper.vm.handleImportSubmit()
|
||||
|
||||
const events = wrapper.emitted('import-success')
|
||||
expect(events).toBeTruthy()
|
||||
expect(events?.[0]?.[0]?.messages).toHaveLength(1)
|
||||
expect(wrapper.vm.importError).toBe('')
|
||||
})
|
||||
|
||||
it('无效 JSON 数据会设置错误信息', async () => {
|
||||
const wrapper = createWrapper()
|
||||
wrapper.vm.importData = 'invalid json'
|
||||
wrapper.vm.selectedImportFormat = 'smart'
|
||||
|
||||
await wrapper.vm.handleImportSubmit()
|
||||
|
||||
expect(wrapper.vm.importError.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('导出预览应该与实际导出结构一致', async () => {
|
||||
const wrapper = createWrapper({
|
||||
mode: 'export',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
tools: [{ type: 'function', function: { name: 'tool', description: 'desc', parameters: {} } }]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const preview = JSON.parse(wrapper.vm.exportPreviewData)
|
||||
expect(preview.messages).toHaveLength(1)
|
||||
expect(preview.tools).toHaveLength(1)
|
||||
expect(preview.metadata.origin).toBe('import_export_dialog')
|
||||
})
|
||||
|
||||
it('导出成功会触发 export-success 并清除错误信息', async () => {
|
||||
const wrapper = createWrapper({
|
||||
mode: 'export',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
})
|
||||
|
||||
await wrapper.vm.handleExportToFile()
|
||||
|
||||
expect(wrapper.emitted('export-success')).toBeTruthy()
|
||||
expect(wrapper.vm.exportError).toBe('')
|
||||
})
|
||||
|
||||
it('导出失败会展示错误并触发 export-error', async () => {
|
||||
mockContextEditor.exportToClipboard.mockResolvedValueOnce(false)
|
||||
const wrapper = createWrapper({
|
||||
mode: 'export',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
})
|
||||
|
||||
await wrapper.vm.handleExportToClipboard()
|
||||
|
||||
expect(wrapper.vm.exportError).toContain('contextEditor.copyFailed')
|
||||
expect(wrapper.emitted('export-error')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('选择 OpenAI 格式时预览应基于转换结果', async () => {
|
||||
const wrapper = createWrapper({
|
||||
mode: 'export',
|
||||
messages: [{ role: 'user', content: 'preview' }]
|
||||
})
|
||||
|
||||
wrapper.vm.selectedExportFormat = 'openai'
|
||||
await nextTick()
|
||||
|
||||
const preview = JSON.parse(wrapper.vm.exportPreviewData)
|
||||
expect(preview.model).toBe('gpt-test')
|
||||
expect(preview.metadata).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user