Files
prompt-optimizer/packages/ui/src/components/basic-mode/BasicSystemWorkspace.vue
T
linshen 204c5d9816 feat(ui): 提升可访问性并稳定 Pro Multi 流程
- 提升 UI 组件可访问性与导航交互一致性\n- 稳定 Pro Multi 消息优化流程,降低 E2E flake
2026-01-13 21:17:00 +08:00

694 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
class="basic-system-workspace"
data-testid="workspace"
data-mode="basic-system"
>
<NFlex
justify="space-between"
:style="{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%',
maxHeight: '100%',
flex: 1,
minHeight: 0,
gap: '16px',
overflow: 'hidden',
}"
>
<!-- 左侧:优化区域 -->
<NFlex
vertical
:style="{ flex: 1, overflow: 'auto', height: '100%', minHeight: 0 }"
size="medium"
>
<!-- 输入控制区域(可折叠) -->
<NCard :style="{ flexShrink: 0 }">
<!-- 折叠态:只显示标题栏 -->
<NFlex
v-if="isInputPanelCollapsed"
justify="space-between"
align="center"
>
<NFlex align="center" :size="8">
<NText :depth="1" style="font-size: 18px; font-weight: 500">
{{ t('promptOptimizer.originalPrompt') }}
</NText>
<NText
v-if="promptModel"
depth="3"
style="font-size: 12px;"
>
{{ promptSummary }}
</NText>
</NFlex>
<NButton
type="tertiary"
size="small"
ghost
round
@click="isInputPanelCollapsed = false"
:title="t('common.expand')"
>
<template #icon>
<NIcon>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</NIcon>
</template>
</NButton>
</NFlex>
<!-- 展开态:完整输入面板 -->
<InputPanelUI
v-else
v-model="promptModel"
test-id-prefix="basic-system"
:label="t('promptOptimizer.originalPrompt')"
:placeholder="t('promptOptimizer.placeholder')"
:model-label="t('promptOptimizer.optimizeModel')"
:template-label="t('promptOptimizer.templateLabel')"
:button-text="t('promptOptimizer.optimize')"
:loading-text="t('common.loading')"
:loading="unwrappedLogicProps.isOptimizing"
:disabled="unwrappedLogicProps.isOptimizing"
:show-preview="false"
:show-analyze-button="true"
:analyze-loading="analyzing"
@submit="logic.handleOptimize"
@analyze="handleAnalyze"
@configModel="handleOpenModelManager"
@open-preview="handleOpenInputPreview"
>
<!-- 模型选择 -->
<template #model-select>
<SelectWithConfig
v-model="selectedOptimizeModelKeyModel"
:options="modelSelection.textModelOptions"
:getPrimary="OptionAccessors.getPrimary"
:getSecondary="OptionAccessors.getSecondary"
:getValue="OptionAccessors.getValue"
@config="handleOpenModelManager"
/>
</template>
<!-- 模板选择 -->
<template #template-select>
<SelectWithConfig
v-model="selectedTemplateIdModel"
:options="templateSelection.templateOptions"
:getPrimary="OptionAccessors.getPrimary"
:getSecondary="OptionAccessors.getSecondary"
:getValue="OptionAccessors.getValue"
@config="() => handleOpenTemplateManager('optimize')"
/>
</template>
<!-- 标题栏折叠按钮 -->
<template #header-extra>
<NButton
type="tertiary"
size="small"
ghost
round
@click="isInputPanelCollapsed = true"
:title="t('common.collapse')"
>
<template #icon>
<NIcon>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
</svg>
</NIcon>
</template>
</NButton>
</template>
</InputPanelUI>
</NCard>
<!-- 优化工作区 -->
<NCard
:style="{ flex: 1, minHeight: '200px', overflow: 'hidden' }"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<PromptPanelUI
test-id="basic-system"
ref="promptPanelRef"
v-model:optimized-prompt="optimizedPromptModel"
:reasoning="unwrappedLogicProps.optimizedReasoning"
:original-prompt="promptModel"
:is-optimizing="unwrappedLogicProps.isOptimizing"
:is-iterating="unwrappedLogicProps.isIterating"
v-model:selected-iterate-template="selectedIterateTemplate"
:versions="unwrappedLogicProps.currentVersions"
:current-version-id="unwrappedLogicProps.currentVersionId"
optimization-mode="system"
:advanced-mode-enabled="false"
:show-preview="false"
@iterate="handleIterate"
@openTemplateManager="handleOpenTemplateManager"
@switchVersion="logic.handleSwitchVersion"
@save-favorite="handleSaveFavorite"
@open-preview="handleOpenPromptPreview"
@apply-improvement="handleApplyImprovement"
@apply-patch="handleApplyPatch"
@save-local-edit="handleSaveLocalEdit"
/>
</NCard>
</NFlex>
<!-- 右侧:测试区域 -->
<TestAreaPanel
ref="testAreaPanelRef"
:style="{ flex: 1, overflow: 'auto', height: '100%', minHeight: 0 }"
optimization-mode="system"
:model-provider="selectedTestModelInfo.provider"
:model-name="selectedTestModelInfo.model"
:optimized-prompt="optimizedPromptModel"
:is-test-running="unwrappedLogicProps.isTestingOriginal || unwrappedLogicProps.isTestingOptimized"
:global-variables="globalVariables"
:predefined-variables="predefinedVariables"
v-model:testContent="testContentModel"
v-model:isCompareMode="isCompareMode"
: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"
:show-evaluation="true"
:has-original-result="hasOriginalResult"
:has-optimized-result="hasOptimizedResult"
:is-evaluating-original="isEvaluatingOriginal"
:is-evaluating-optimized="isEvaluatingOptimized"
:original-score="originalScore"
:optimized-score="optimizedScore"
:has-original-evaluation="hasOriginalEvaluation"
:has-optimized-evaluation="hasOptimizedEvaluation"
:original-evaluation-result="originalEvaluationResult"
:optimized-evaluation-result="optimizedEvaluationResult"
:original-score-level="originalScoreLevel"
:optimized-score-level="optimizedScoreLevel"
@test="logic.handleTest"
@open-variable-manager="handleOpenVariableManager"
@evaluate-original="() => handleEvaluate('original')"
@evaluate-optimized="() => handleEvaluate('optimized')"
@show-original-detail="() => showDetail('original')"
@show-optimized-detail="() => showDetail('optimized')"
@apply-improvement="handleApplyImprovement"
@apply-patch="handleApplyPatch"
>
<template #model-select>
<SelectWithConfig
v-model="selectedTestModelKeyModel"
:options="modelSelection.textModelOptions"
:getPrimary="OptionAccessors.getPrimary"
:getSecondary="OptionAccessors.getSecondary"
:getValue="OptionAccessors.getValue"
@config="handleOpenModelManager"
/>
</template>
<template #original-result>
<OutputDisplay
:content="unwrappedLogicProps.testResultsOriginalResult"
:reasoning="unwrappedLogicProps.testResultsOriginalReasoning"
:streaming="unwrappedLogicProps.isTestingOriginal"
:enableDiff="false"
mode="readonly"
:style="{ height: '100%', minHeight: '0' }"
/>
</template>
<template #optimized-result>
<OutputDisplay
:content="unwrappedLogicProps.testResultsOptimizedResult"
:reasoning="unwrappedLogicProps.testResultsOptimizedReasoning"
:streaming="unwrappedLogicProps.isTestingOptimized"
:enableDiff="false"
mode="readonly"
:style="{ height: '100%', minHeight: '0' }"
/>
</template>
<template #single-result>
<OutputDisplay
:content="unwrappedLogicProps.testResultsOptimizedResult"
:reasoning="unwrappedLogicProps.testResultsOptimizedReasoning"
:streaming="unwrappedLogicProps.isTestingOptimized"
:enableDiff="false"
mode="readonly"
:style="{ height: '100%', minHeight: '0' }"
/>
</template>
<template #custom-actions>
<template v-if="isCompareMode && unwrappedLogicProps.testResultsOriginalResult && unwrappedLogicProps.testResultsOptimizedResult">
<EvaluationScoreBadge
v-if="hasCompareEvaluation || isEvaluatingCompare"
:score="compareScore"
:level="compareScoreLevel"
:loading="isEvaluatingCompare"
:result="compareEvaluationResult"
type="compare"
size="small"
@show-detail="() => showDetail('compare')"
@apply-improvement="handleApplyImprovement"
@apply-patch="handleApplyPatch"
/>
<NButton
v-else
quaternary
size="small"
@click="() => handleEvaluate('compare')"
>
{{ t('evaluation.compareEvaluate') }}
</NButton>
</template>
</template>
</TestAreaPanel>
</NFlex>
</div>
</template>
<script setup lang="ts">
/**
* BasicSystemWorkspace - Basic 模式 System 子模式工作区
*
* 职责:
* - 直接使用 useBasicSystemSession 作为状态源
* - 使用 useBasicWorkspaceLogic 处理业务逻辑
* - 使用 useWorkspaceModelSelection 管理模型选择
* - 使用 useWorkspaceTemplateSelection 管理模板选择
* - 使用 useEvaluationHandler 处理评估功能
* - 内联基础模式工作区布局(与 BasicUserWorkspace 保持一致)
*/
import { ref, computed, inject, onMounted, onUnmounted, watch, nextTick, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '../../composables/ui/useToast'
import { useBasicSystemSession } from '../../stores/session/useBasicSystemSession'
import { useBasicWorkspaceLogic } from '../../composables/workspaces/useBasicWorkspaceLogic'
import { useWorkspaceModelSelection } from '../../composables/workspaces/useWorkspaceModelSelection'
import { useWorkspaceTemplateSelection } from '../../composables/workspaces/useWorkspaceTemplateSelection'
import { useEvaluationHandler } from '../../composables/prompt/useEvaluationHandler'
import { provideEvaluation } from '../../composables/prompt/useEvaluationContext'
import { NButton, NCard, NFlex, NIcon, NText } from 'naive-ui'
import InputPanelUI from '../InputPanel.vue'
import PromptPanelUI from '../PromptPanel.vue'
import TestAreaPanel from '../TestAreaPanel.vue'
import OutputDisplay from '../OutputDisplay.vue'
import { EvaluationScoreBadge } from '../evaluation'
import SelectWithConfig from '../SelectWithConfig.vue'
import { OptionAccessors } from '../../utils/data-transformer'
import type { AppServices } from '../../types/services'
import type { IteratePayload } from '../../types/workspace'
import { applyPatchOperationsToText, type PatchOperation, type Template } from '@prompt-optimizer/core'
import type { TestAreaPanelInstance } from '../types/test-area'
const { t } = useI18n()
const toast = useToast()
// 服务注入
const injectedServices = inject<Ref<AppServices | null> | null>('services', null)
const services = injectedServices ?? ref<AppServices | null>(null)
const appOpenModelManager = inject<((tab?: 'text' | 'image' | 'function') => void) | null>('openModelManager', null)
const appOpenTemplateManager = inject<((type?: string) => void) | null>('openTemplateManager', null)
// Session store(单一真源)
const session = useBasicSystemSession()
// 业务逻辑
const logic = useBasicWorkspaceLogic({
services,
sessionStore: session,
optimizationMode: 'system',
promptRecordType: 'optimize',
onOptimizeComplete: (_chain) => {
// 发送历史刷新事件
window.dispatchEvent(new CustomEvent('prompt-optimizer:history-refresh'))
},
onIterateComplete: (_chain) => {
window.dispatchEvent(new CustomEvent('prompt-optimizer:history-refresh'))
}
})
// 模型选择
const modelSelection = useWorkspaceModelSelection(services, session)
// 模板选择(templateType: 'optimize', iterateTemplateType: 'iterate')
const templateSelection = useWorkspaceTemplateSelection(
services,
session,
'optimize',
'iterate'
)
// 迭代模板(从 session 派生,持久化)
const selectedIterateTemplate = computed<Template | null>({
get: () => templateSelection.selectedIterateTemplate.value,
set: (value) => {
templateSelection.selectedIterateTemplateId.value = value?.id ?? ''
templateSelection.selectedIterateTemplate.value = value ?? null
}
})
// 对比模式(从 session store 读取)
const isCompareMode = computed<boolean>({
get: () => !!session.isCompareMode,
set: (value) => session.toggleCompareMode(!!value)
})
// 派生状态
// ✅ 修复:处理 testResults 可能为 null 的情况
const hasOriginalResult = computed(() => !!logic.testResults.value?.originalResult)
const hasOptimizedResult = computed(() => !!logic.testResults.value?.optimizedResult)
// 组件引用(用于触发迭代对话框、刷新迭代下拉等)
type PromptPanelExpose = {
openIterateDialog?: (initialContent?: string) => void
refreshIterateTemplateSelect?: () => void
} | null
const promptPanelRef = ref<PromptPanelExpose>(null)
const testAreaPanelRef = ref<TestAreaPanelInstance | null>(null)
// 输入区折叠状态(初始展开)
const isInputPanelCollapsed = ref(false)
// 提示词摘要(折叠态显示)
const promptSummary = computed(() => {
const prompt = logic.prompt.value
if (!prompt) return ''
return prompt.length > 50 ? prompt.slice(0, 50) + '...' : prompt
})
// 分析评估(prompt-only):收起输入区后触发评估
const handleAnalyze = async () => {
if (!logic.prompt.value?.trim()) return
if (logic.isOptimizing.value) return
if (analyzing.value) return
analyzing.value = true
try {
// 分析模式不产生新提示词,但评估请求需要 non-empty optimizedPrompt
// 将当前原始提示词同步到 optimizedPrompt,供 prompt-only 评估使用
logic.optimizedPrompt.value = logic.prompt.value
logic.optimizedReasoning.value = ''
isInputPanelCollapsed.value = true
await nextTick()
await handleAnalyzeEvaluate()
} finally {
analyzing.value = false
}
}
// 🔧 解包 logic 中的 ref,用于传递给子组件(避免 Vue prop 类型警告)
const unwrappedLogicProps = computed(() => ({
isOptimizing: logic.isOptimizing.value,
isIterating: logic.isIterating.value,
currentVersions: logic.currentVersions.value,
currentVersionId: logic.currentVersionId.value,
isTestingOriginal: logic.isTestingOriginal.value,
isTestingOptimized: logic.isTestingOptimized.value,
optimizedReasoning: logic.optimizedReasoning.value,
// ✅ 修复:处理 testResults 可能为 null 的情况
testResultsOriginalResult: logic.testResults.value?.originalResult || '',
testResultsOriginalReasoning: logic.testResults.value?.originalReasoning || '',
testResultsOptimizedResult: logic.testResults.value?.optimizedResult || '',
testResultsOptimizedReasoning: logic.testResults.value?.optimizedReasoning || ''
}))
// 🔧 为 v-model 创建解包的 computed(支持双向绑定)
const promptModel = computed({
get: () => logic.prompt.value,
set: (value) => { logic.prompt.value = value }
})
const optimizedPromptModel = computed({
get: () => logic.optimizedPrompt.value,
set: (value) => { logic.optimizedPrompt.value = value }
})
const testContentModel = computed({
get: () => logic.testContent.value,
set: (value) => { logic.testContent.value = value }
})
// 🔧 为 SelectWithConfig 的 v-model 创建解包的 computed
const selectedOptimizeModelKeyModel = computed({
get: () => logic.selectedOptimizeModelKey.value,
set: (value) => { logic.selectedOptimizeModelKey.value = value }
})
const selectedTemplateIdModel = computed({
get: () => logic.selectedTemplateId.value,
set: (value) => { logic.selectedTemplateId.value = value }
})
const selectedTestModelKeyModel = computed({
get: () => logic.selectedTestModelKey.value,
set: (value) => { logic.selectedTestModelKey.value = value }
})
// 测试模型信息
const selectedTestModelInfo = computed(() => modelSelection.selectedTestModelInfo.value)
// 评估处理器
// ✅ 修复:处理 testResults 可能为 null 的情况,添加 .value 访问
const testResultsComputed = computed(() => ({
originalResult: logic.testResults.value?.originalResult || undefined,
optimizedResult: logic.testResults.value?.optimizedResult || undefined
}))
const evaluationHandler = useEvaluationHandler({
services,
originalPrompt: logic.prompt,
optimizedPrompt: logic.optimizedPrompt,
testContent: logic.testContent,
testResults: testResultsComputed,
evaluationModelKey: logic.selectedTestModelKey,
functionMode: computed(() => 'basic'),
subMode: computed(() => 'system'),
currentIterateRequirement: computed(() => {
const versionId = logic.currentVersionId.value
if (!versionId || !logic.currentVersions.value) return ''
const currentVersion = logic.currentVersions.value.find(v => v.id === versionId)
return currentVersion?.iterationNote || ''
})
})
// 提供评估上下文
provideEvaluation(evaluationHandler.evaluation)
// 评估状态
const { evaluation, handleEvaluate: handleEvaluateInternal } = evaluationHandler
const testAreaProps = evaluationHandler.testAreaEvaluationProps
const isEvaluatingOriginal = computed(() => testAreaProps.value.isEvaluatingOriginal)
const isEvaluatingOptimized = computed(() => testAreaProps.value.isEvaluatingOptimized)
const originalScore = computed(() => testAreaProps.value.originalScore ?? 0)
const optimizedScore = computed(() => testAreaProps.value.optimizedScore ?? 0)
const hasOriginalEvaluation = computed(() => testAreaProps.value.hasOriginalEvaluation)
const hasOptimizedEvaluation = computed(() => testAreaProps.value.hasOptimizedEvaluation)
const originalEvaluationResult = computed(() => testAreaProps.value.originalEvaluationResult)
const optimizedEvaluationResult = computed(() => testAreaProps.value.optimizedEvaluationResult)
const originalScoreLevel = computed(() => testAreaProps.value.originalScoreLevel)
const optimizedScoreLevel = computed(() => testAreaProps.value.optimizedScoreLevel)
// 对比评估状态
const isEvaluatingCompare = evaluationHandler.compareEvaluation.isEvaluatingCompare
const compareScore = computed(() => evaluationHandler.compareEvaluation.compareScore.value ?? 0)
const hasCompareEvaluation = evaluationHandler.compareEvaluation.hasCompareResult
const compareEvaluationResult = computed(() => evaluation.state['compare'])
const compareScoreLevel = computed(() => evaluation.getScoreLevel('compare'))
// 占位状态
const globalVariables = ref({})
const predefinedVariables = ref({})
const inputMode = ref('normal')
const controlBarLayout = ref('horizontal')
const buttonSize = ref('medium')
const conversationMaxHeight = ref(600)
const resultVerticalLayout = ref(false)
const analyzing = ref(false)
// ==================== 事件处理 ====================
// 迭代优化
const handleIterate = (payload: IteratePayload) => {
logic.handleIterate(payload)
}
// 评估
const handleEvaluate = async (type: 'original' | 'optimized' | 'compare') => {
console.log('[BasicSystemWorkspace] evaluate', type)
await handleEvaluateInternal(type)
}
// 分析评估(prompt-only)
const handleAnalyzeEvaluate = async () => {
console.log('[BasicSystemWorkspace] analyzeEvaluate (prompt-only)')
await handleEvaluateInternal('prompt-only')
}
// 显示详情
const showDetail = (type: 'original' | 'optimized' | 'compare') => {
console.log('[BasicSystemWorkspace] showDetail', type)
evaluation.showDetail(type)
}
// 应用改进
const handleApplyImprovement = (payload: { improvement: string; type: string }) => {
evaluation.closePanel()
promptPanelRef.value?.openIterateDialog?.(payload.improvement)
}
// 应用补丁
const handleApplyPatch = (payload: { operation: PatchOperation }) => {
if (!payload.operation) return
const current = logic.optimizedPrompt.value || ''
const result = applyPatchOperationsToText(current, payload.operation)
if (!result.ok) {
toast.warning(t('toast.warning.patchApplyFailed'))
return
}
logic.optimizedPrompt.value = result.text
}
// 保存本地编辑
const handleSaveLocalEdit = (payload: any) => {
console.log('[BasicSystemWorkspace] saveLocalEdit', payload)
toast.info('保存编辑功能开发中')
}
// 保存收藏(从顶层 App 注入)
const globalHandleSaveFavorite = inject<((data: { content: string; originalContent?: string }) => void) | null>(
'handleSaveFavorite',
null
)
const handleSaveFavorite = () => {
console.log('[BasicSystemWorkspace] saveFavorite')
if (!globalHandleSaveFavorite) {
toast.error(t('toast.error.favoriteNotInitialized'))
return
}
const data = {
content: logic.optimizedPrompt.value || logic.prompt.value,
originalContent: logic.prompt.value
}
if (!data.content && !data.originalContent) {
toast.warning(t('toast.error.noContentToSave'))
return
}
globalHandleSaveFavorite(data)
}
// 打开变量管理器
const handleOpenVariableManager = (_variableName?: string) => {
toast.info('变量管理器功能开发中')
}
// 打开输入预览
const handleOpenInputPreview = () => {
console.log('[BasicSystemWorkspace] openInputPreview')
toast.info('输入预览功能开发中')
}
// 打开提示词预览
const handleOpenPromptPreview = () => {
console.log('[BasicSystemWorkspace] openPromptPreview')
toast.info('提示词预览功能开发中')
}
// 打开模型管理器
const handleOpenModelManager = () => {
appOpenModelManager?.('text')
}
// 打开模板管理器
const handleOpenTemplateManager = (type?: string) => {
appOpenTemplateManager?.(type || 'optimize')
}
// ==================== 初始化 ====================
onMounted(async () => {
// 加载版本列表
await logic.loadVersions()
// 刷新模型和模板列表
await modelSelection.refreshTextModels()
await templateSelection.refreshOptimizeTemplates()
await templateSelection.refreshIterateTemplates()
if (typeof window !== 'undefined') {
window.addEventListener('basic-workspace-refresh-text-models', refreshTextModelsHandler)
window.addEventListener('basic-workspace-refresh-templates', refreshTemplatesHandler)
window.addEventListener('basic-workspace-refresh-iterate-select', refreshIterateSelectHandler)
}
})
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('basic-workspace-refresh-text-models', refreshTextModelsHandler)
window.removeEventListener('basic-workspace-refresh-templates', refreshTemplatesHandler)
window.removeEventListener('basic-workspace-refresh-iterate-select', refreshIterateSelectHandler)
}
})
const refreshTextModelsHandler = async () => {
await modelSelection.refreshTextModels()
}
const refreshTemplatesHandler = async () => {
await templateSelection.refreshOptimizeTemplates()
await templateSelection.refreshIterateTemplates()
await nextTick()
promptPanelRef.value?.refreshIterateTemplateSelect?.()
}
const refreshIterateSelectHandler = async () => {
await nextTick()
promptPanelRef.value?.refreshIterateTemplateSelect?.()
}
// chainId 变化时加载版本
watch(() => session.chainId, async (newChainId) => {
if (newChainId) {
await logic.loadVersions()
} else {
logic.currentVersions.value = []
logic.currentChainId.value = ''
logic.currentVersionId.value = ''
}
})
defineExpose({
promptPanelRef,
testAreaPanelRef,
openIterateDialog: (initialContent?: string) => {
promptPanelRef.value?.openIterateDialog?.(initialContent)
}
})
</script>
<style scoped>
.basic-system-workspace {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
</style>