feat(ui): 实现三大功能模式的独立子模式持久化功能

- 新增三个子模式管理Composable (useBasicSubMode/useProSubMode/useImageSubMode)
- 实现基础/上下文/图像模式的完全独立状态存储
- 添加UI_SETTINGS_KEYS常量用于子模式存储键管理
- 更新App.vue初始化逻辑支持三模式独立恢复
- 修复图像模式刷新后文件上传按钮不显示的bug
- 完善历史记录和收藏恢复时的子模式持久化
- 新增国际化文本支持子模式切换提示
- 归档完整开发文档到126-submode-persistence

核心特性:
- 状态隔离: 三个功能模式维护完全独立的子模式状态
- 跨页面同步: 使用自定义事件实现组件间状态同步
- 双层状态一致性: 导航层和组件层状态保持同步
- 异步初始化: 非阻塞式状态恢复机制
This commit is contained in:
linshen
2025-10-25 10:31:14 +08:00
parent c8e06bdfb0
commit 947ede8a1d
22 changed files with 5463 additions and 1738 deletions

View File

@@ -0,0 +1,140 @@
# 126 - 子模式持久化与导航栏统一
## 📋 功能概述
实现三种功能模式(基础/上下文/图像)的子模式独立持久化,并将所有子模式选择器统一移至导航栏,提升用户体验的一致性。
## ⏱️ 时间线
- **开始时间**: 2025-10-22
- **完成时间**: 2025-10-22
- **总耗时**: 约8小时
## 🎯 核心目标
### 主要目标
1. ✅ 实现三种功能模式的子模式独立持久化
2. ✅ 将所有子模式选择器移至导航栏
3. ✅ 确保状态完全隔离(基础和上下文模式虽然子模式名称相同,但独立存储)
4. ✅ 修复图像模式初始化时imageMode未恢复的问题
### 次要目标
1. ✅ 保持向后兼容(与旧变量同步)
2. ✅ 完善的错误处理和日志
3. ✅ 全面的测试验证
## 📊 实施状态
**状态**: ✅ 已完成
### 完成的工作
#### Phase 1: 上下文模式子模式持久化
- ✅ 添加 `PRO_SUB_MODE` 存储键
- ✅ 定义 `ProSubMode` 类型
- ✅ 创建 `useProSubMode` composable
- ✅ 集成到 App.vue
- ✅ 测试验证
#### Phase 2: 基础模式子模式持久化
- ✅ 添加 `BASIC_SUB_MODE` 存储键
- ✅ 定义 `BasicSubMode` 类型
- ✅ 创建 `useBasicSubMode` composable
- ✅ 集成到 App.vue
- ✅ 验证独立性
#### Phase 3: 图像模式子模式持久化
- ✅ 添加 `IMAGE_SUB_MODE` 存储键
- ✅ 定义 `ImageSubMode` 类型
- ✅ 创建 `useImageSubMode` composable
- ✅ 移动 ImageModeSelector 到导航栏
- ✅ 通过自定义事件通信
- ✅ 修复初始化恢复问题
## 🐛 已解决的问题
### 问题1: 基础模式子模式选择器缺失
**现象**: 只有上下文模式显示子模式选择器,基础模式的选择器不见了
**原因**: `v-if` 条件只判断了 `functionMode === 'pro'`
**解决**: 改为独立显示三个选择器
### 问题2: 状态共享导致混淆
**现象**: 基础模式和上下文模式的子模式选择相互影响
**原因**: 使用同一个 `selectedOptimizationMode` 变量
**解决**: 完全独立的存储和状态管理
### 问题3: 图像模式刷新后文件上传区域不显示
**现象**: 从文生图切换到图生图时正常,但刷新页面后文件上传按钮不显示
**原因**: `useImageWorkspace``restoreSelections` 方法未恢复 `imageMode`
**解决**: 在 `restoreSelections` 中添加从 `UI_SETTINGS_KEYS.IMAGE_SUB_MODE` 恢复的逻辑
## 📁 文件结构
```
docs/archives/126-submode-persistence/
├── README.md # 本文件 - 功能概述
├── design.md # 完整的设计与实施文档v4.0
├── implementation.md # 实施详情和代码示例
└── experience.md # 经验总结和最佳实践
```
## 🔑 核心设计原则
### 1. 状态完全隔离
三种功能模式使用完全独立的存储键和Composable即使子模式名称相同也不共享状态。
**用户的关键洞察**:
> "基础模式也应该有自己的存储,这个也应该分开...因为这两个功能模式本质上控制的是不同的,只是当前他们的子模式碰巧都叫 系统/用户提示词优化而已。"
### 2. 单例模式的全局状态
每个Composable内部维护单例状态确保全局唯一避免多实例冲突。
### 3. 异步初始化
不阻塞应用启动,通过 `ensureInitialized()` 延迟加载,并带有防抖机制。
### 4. 自动持久化
每次子模式切换自动保存到localStorage用户无感知。
## 📈 技术亮点
1. **完整的状态隔离**: 三个独立的存储键和Composable
2. **统一的UI体验**: 所有子模式选择器都在导航栏
3. **完善的错误处理**: 初始化失败时回退到默认值
4. **清晰的日志输出**: 便于调试和问题排查
5. **向后兼容**: 保留旧变量,平滑升级
## 🔗 相关文档
- [design.md](./design.md) - 完整的设计文档包含v1.0-v4.0演进历史)
- [implementation.md](./implementation.md) - 详细的实施记录和代码
- [experience.md](./experience.md) - 经验总结和最佳实践
## 📝 使用说明
### 开发者参考
1. 查看 [design.md](./design.md) 了解完整的设计思路和架构决策
2. 查看 [implementation.md](./implementation.md) 了解具体实现细节
3. 查看 [experience.md](./experience.md) 学习经验和最佳实践
### 问题排查
如遇到子模式相关问题,参考 [experience.md](./experience.md) 的常见问题部分。
## ✨ 成功指标
- ✅ 所有三种模式的子模式能正确持久化
- ✅ 刷新页面后状态完全保持
- ✅ 功能模式切换时各自恢复独立的子模式
- ✅ 历史记录和收藏恢复时正确切换子模式
- ✅ 无编译错误和运行时错误
- ✅ 性能无明显下降
- ✅ 所有测试场景通过
## 🎓 经验总结
详见 [experience.md](./experience.md)
---
**文档版本**: v1.0
**最后更新**: 2025-10-22
**维护者**: Claude & 用户

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,452 @@
# 子模式持久化 - 经验总结
## 💡 核心经验
### 1. 状态隔离的重要性
**关键洞察(来自用户)**:
> "基础模式也应该有自己的存储,这个也应该分开...因为这两个功能模式本质上控制的是不同的,只是当前他们的子模式碰巧都叫 系统/用户提示词优化而已。"
**经验总结**:
-**名称相同 ≠ 状态共享**: 即使子模式名称相同(如都叫"系统/用户"),也应该独立存储
-**功能模式是第一维度**: 不同的功能模式代表不同的使用场景
-**用户心智模型**: 用户期望每个功能模式"记住"自己上次的选择
**反模式**:
```typescript
// ❌ 错误: 共享状态
const selectedOptimizationMode = ref<'system' | 'user'>('system')
// 基础模式和上下文模式都使用同一个变量
// 导致切换功能模式时状态混乱
```
**最佳实践**:
```typescript
// ✅ 正确: 完全独立的状态
const { basicSubMode } = useBasicSubMode(services)
const { proSubMode } = useProSubMode(services)
// 各自独立存储,互不影响
```
---
### 2. 单例模式的正确使用
**问题背景**: Composable可能被多次调用如何确保状态唯一
**解决方案**:
```typescript
let singleton: {
mode: Ref<SubModeType>
initialized: boolean
initializing: Promise<void> | null
} | null = null
export function useSubMode(services: Ref<AppServices | null>) {
if (!singleton) {
singleton = {
mode: ref<SubModeType>('default'),
initialized: false,
initializing: null
}
}
// ...
}
```
**关键点**:
1. **模块级变量**: `singleton` 在模块作用域,确保全局唯一
2. **惰性初始化**: 首次调用时创建
3. **状态共享**: 后续调用返回同一个状态引用
**常见陷阱**:
```typescript
// ❌ 错误: 每次调用都创建新状态
export function useSubMode() {
const mode = ref('default') // 每次都是新的!
// ...
}
```
---
### 3. 异步初始化的防抖处理
**问题**: 如果多个组件同时调用 `ensureInitialized()`,会导致重复读取存储。
**解决方案**:
```typescript
const ensureInitialized = async () => {
// 第一层防护:已初始化
if (singleton!.initialized) return
// 第二层防护:正在初始化(防抖)
if (singleton!.initializing) {
await singleton!.initializing
return
}
// 记录初始化Promise
singleton!.initializing = (async () => {
try {
// 实际初始化逻辑
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
```
**关键机制**:
1. **双重检查**: `initialized` + `initializing`
2. **Promise共享**: 多个调用者等待同一个Promise
3. **finally保证**: 无论成功失败都清理状态
---
### 4. 只读状态暴露模式
**为什么需要只读?**
- 防止外部直接修改状态
- 强制通过setter进行更新便于持久化
- 更好的代码可维护性
**实现方式**:
```typescript
import { readonly } from 'vue'
return {
// ✅ 只读: 外部不能直接修改
basicSubMode: readonly(singleton.mode) as Ref<BasicSubMode>,
// ✅ 修改器: 通过setter更新并持久化
setBasicSubMode: async (mode: BasicSubMode) => {
singleton!.mode.value = mode
await setPreference(STORAGE_KEY, mode)
}
}
```
**避免的陷阱**:
```typescript
// ❌ 错误: 直接暴露可写状态
return {
basicSubMode: singleton.mode, // 外部可以直接修改!
// ...
}
// 导致问题:
basicSubMode.value = 'user' // 修改了状态但没有持久化!
```
---
### 5. 跨组件通信策略
**场景**: 导航栏的选择器在 App.vue但 ImageWorkspace 内部需要知道切换事件。
**方案对比**:
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| Props传递 | 简单直接 | 组件耦合高 | 父子组件 |
| Provide/Inject | 解耦 | 需要共同父组件 | 深层嵌套 |
| 自定义事件 | 完全解耦 | 需要手动管理 | 跨层级通信 |
| Composable共享 | 类型安全 | 需要单例模式 | 全局状态 |
**本项目选择**:
- **导航栏→App.vue**: Composable共享状态
- **App.vue→ImageWorkspace**: 自定义事件
**自定义事件实现**:
```typescript
// 发送端App.vue
window.dispatchEvent(new CustomEvent("image-submode-changed", {
detail: { mode }
}))
// 接收端ImageWorkspace.vue
const handleImageSubModeChanged = (e: CustomEvent) => {
const { mode } = e.detail
if (mode && mode !== imageMode.value) {
handleImageModeChange(mode)
}
}
onMounted(() => {
window.addEventListener("image-submode-changed", handleImageSubModeChanged as EventListener)
})
onBeforeUnmount(() => {
window.removeEventListener("image-submode-changed", handleImageSubModeChanged as EventListener)
})
```
---
### 6. 双层状态同步问题
**问题发现**: 图像模式刷新后文件上传按钮不显示
**原因分析**:
```
导航栏层 (App.vue + useImageSubMode)
✅ 从 UI_SETTINGS_KEYS.IMAGE_SUB_MODE 恢复
✅ 导航栏显示正确
组件内部层 (ImageWorkspace + useImageWorkspace)
❌ 没有从存储恢复
❌ 始终使用硬编码默认值 'text2image'
❌ v-if="imageMode === 'image2image'" 永远为 false
```
**解决方案**: 两层都从同一个存储键恢复
```typescript
// useImageWorkspace.ts
const restoreSelections = async () => {
// ... 其他恢复 ...
// ✅ 从全局存储恢复
const savedImageMode = await getPreference(
UI_SETTINGS_KEYS.IMAGE_SUB_MODE, // 与导航栏使用同一个键!
"text2image",
)
if (savedImageMode === "text2image" || savedImageMode === "image2image") {
state.imageMode = savedImageMode
}
}
```
**经验教训**:
-**统一数据源**: 所有层级都从同一个存储键读取
-**初始化检查**: 确保所有使用状态的地方都正确初始化
-**日志追踪**: 在初始化和切换时输出日志,便于发现问题
---
### 7. 向后兼容策略
**挑战**: 现有代码大量使用 `selectedOptimizationMode``contextMode`
**策略**: 保留旧变量与新Composable同步
```typescript
// 新状态
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services)
const { proSubMode, setProSubMode } = useProSubMode(services)
// 旧变量(保留兼容)
const selectedOptimizationMode = ref<OptimizationMode>("system")
// 切换时同步
const handleBasicSubModeChange = async (mode: OptimizationMode) => {
await setBasicSubMode(mode as BasicSubMode)
selectedOptimizationMode.value = mode // ✅ 同步旧变量
}
```
**优点**:
1. 降低重构风险
2. 平滑升级
3. 避免大范围改动
**长期计划**:
- 逐步迁移使用处到新API
- 最终废弃旧变量
---
## 🎯 设计模式总结
### 1. 单例模式 (Singleton Pattern)
**用途**: 确保全局唯一状态
**实现**: 模块级变量 + 惰性初始化
### 2. 代理模式 (Proxy Pattern)
**用途**: 控制状态访问
**实现**: readonly() 包装 + setter方法
### 3. 观察者模式 (Observer Pattern)
**用途**: 跨组件通信
**实现**: 自定义事件 + addEventListener
### 4. 策略模式 (Strategy Pattern)
**用途**: 根据功能模式选择不同处理
**实现**: if-else分支 + 独立的Composable
---
## 🚫 常见陷阱
### 陷阱1: 忘记初始化
```typescript
// ❌ 错误
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services)
setBasicSubMode('user') // 可能在初始化前调用!
// ✅ 正确
const { basicSubMode, setBasicSubMode, ensureInitialized } = useBasicSubMode(services)
await ensureInitialized() // 先初始化
await setBasicSubMode('user')
```
### 陷阱2: 直接修改只读状态
```typescript
// ❌ 错误
basicSubMode.value = 'user' // TypeScript会报错
// ✅ 正确
await setBasicSubMode('user')
```
### 陷阱3: 忘记清理事件监听
```typescript
// ❌ 错误: 只注册不清理
onMounted(() => {
window.addEventListener("event", handler)
})
// ✅ 正确: 清理避免内存泄漏
onMounted(() => {
window.addEventListener("event", handler)
})
onBeforeUnmount(() => {
window.removeEventListener("event", handler)
})
```
### 陷阱4: 状态类型混淆
```typescript
// ❌ 错误: 类型混用
const mode: ProSubMode = basicSubMode.value // 类型不匹配!
// ✅ 正确: 类型转换
const mode = basicSubMode.value as OptimizationMode
```
---
## 📊 性能考虑
### 1. 初始化性能
-**异步加载**: 不阻塞应用启动
-**防抖机制**: 避免重复读取
-**单次读取**: localStorage读取很快无需缓存
### 2. 切换性能
-**响应式更新**: Vue自动处理几乎无开销
-**局部更新**: 只更新相关组件
-**异步持久化**: 不阻塞UI
### 3. 内存占用
-**单例模式**: 只有一个状态实例
-**轻量数据**: 只存储字符串值
-**事件清理**: 避免内存泄漏
---
## 🧪 测试经验
### 测试策略
1. **单元测试**: Composable的核心逻辑
2. **集成测试**: App.vue的初始化和切换
3. **手动测试**: 实际使用场景验证
### 关键测试场景
1. ✅ 首次使用(无存储数据)
2. ✅ 刷新页面后状态保持
3. ✅ 功能模式切换时各自恢复
4. ✅ 独立性验证(基础/上下文不互相影响)
5. ✅ 历史记录恢复
6. ✅ 收藏恢复
### 调试技巧
1. **日志输出**: 每个关键操作都输出日志
2. **localStorage检查**: 浏览器开发工具查看存储
3. **响应式追踪**: Vue DevTools查看状态变化
---
## 📝 文档化经验
### 1. 渐进式文档
- **v1.0**: 初始设计(仅上下文模式)
- **v2.0**: 添加基础模式
- **v3.0**: 添加图像模式
- **v4.0**: 完成并归档
### 2. 记录决策
- 用户的关键洞察要高亮
- 技术决策要说明理由
- 遇到的问题要记录原因和解决方案
### 3. 代码示例
- 提供完整的代码片段
- 标注关键行
- 对比正确和错误的写法
---
## 🎓 可复用经验
### 适用场景
本架构适用于以下场景:
1. **多模式应用**: 有多个独立的功能模式
2. **状态持久化**: 需要记住用户选择
3. **全局状态**: 需要在多个组件间共享
4. **类型安全**: TypeScript项目
### 扩展建议
添加新功能模式时:
1.`storage-keys.ts` 添加存储键
2.`types.ts` 定义类型
3. 创建对应的 `useXxxSubMode.ts`
4. 在 App.vue 中集成
5. 添加测试验证
---
## 💡 关键建议
### 给开发者
1.**状态隔离优于共享**: 默认独立存储,除非有明确的共享需求
2.**单例模式解决重复**: 需要全局状态时使用单例模式
3.**异步初始化**: 避免阻塞应用启动
4.**只读状态**: 防止意外修改强制通过setter
5.**完善日志**: 便于调试和问题排查
### 给架构师
1.**用户心智模型第一**: 技术实现要符合用户直觉
2.**向后兼容**: 重构时保留旧接口,平滑升级
3.**防御式编程**: 完善的错误处理和回退机制
4.**文档跟进**: 及时记录设计决策和演进过程
---
## 🔮 未来改进
### 短期(已完成)
- ✅ 三种模式全部独立持久化
- ✅ 统一的导航栏UI
- ✅ 修复图像模式初始化问题
### 中期(待讨论)
- 🔄 废弃 `selectedOptimizationMode` 变量
- 🔄 统一 `contextMode``proSubMode`
- 🔄 术语统一OptimizationMode → SubMode
### 长期(可选)
- 💡 支持更多功能模式
- 💡 子模式配置化(通过配置文件定义)
- 💡 更细粒度的持久化控制
---
**文档版本**: v1.0
**最后更新**: 2025-10-22
**贡献者**: Claude & 用户

View File

@@ -0,0 +1,529 @@
# 子模式持久化 - 实施记录
## 📋 实施概览
本文档记录三种功能模式子模式持久化的完整实施过程,包括核心代码、关键决策和实施步骤。
## 🔧 核心实现
### 1. 存储键定义
**文件**: `packages/core/src/constants/storage-keys.ts`
```typescript
export const UI_SETTINGS_KEYS = {
// ... 现有键 ...
FUNCTION_MODE: 'app:settings:ui:function-mode',
// ✅ 子模式持久化(三种功能模式独立存储)
BASIC_SUB_MODE: 'app:settings:ui:basic-sub-mode', // 基础模式
PRO_SUB_MODE: 'app:settings:ui:pro-sub-mode', // 上下文模式
IMAGE_SUB_MODE: 'app:settings:ui:image-sub-mode', // 图像模式
} as const
```
**设计要点**:
- 三个完全独立的存储键
- 命名清晰反映功能模式
- 使用 `as const` 确保类型安全
---
### 2. TypeScript类型定义
**文件**: `packages/core/src/services/prompt/types.ts`
```typescript
/**
* 子模式类型定义(三种功能模式独立)
* 用于持久化各功能模式下的子模式选择
*/
// 基础模式的子模式
export type BasicSubMode = "system" | "user"
// 上下文模式的子模式
export type ProSubMode = "system" | "user"
// 图像模式的子模式
export type ImageSubMode = "text2image" | "image2image"
```
**设计要点**:
- 三个独立的类型,即使值域相同也不混用
- 清晰的JSDoc注释
- 体现功能模式的独立性
---
### 3. Composable实现
#### useBasicSubMode.ts
**文件**: `packages/ui/src/composables/useBasicSubMode.ts`
```typescript
import { ref, readonly, type Ref } from 'vue'
import type { AppServices } from '../types/services'
import { usePreferences } from './usePreferenceManager'
import { UI_SETTINGS_KEYS, type BasicSubMode } from '@prompt-optimizer/core'
interface UseBasicSubModeApi {
basicSubMode: Ref<BasicSubMode>
setBasicSubMode: (mode: BasicSubMode) => Promise<void>
switchToSystem: () => Promise<void>
switchToUser: () => Promise<void>
ensureInitialized: () => Promise<void>
}
let singleton: {
mode: Ref<BasicSubMode>
initialized: boolean
initializing: Promise<void> | null
} | null = null
export function useBasicSubMode(services: Ref<AppServices | null>): UseBasicSubModeApi {
// 单例模式:确保全局唯一状态
if (!singleton) {
singleton = {
mode: ref<BasicSubMode>('system'),
initialized: false,
initializing: null
}
}
const { getPreference, setPreference } = usePreferences(services)
// 异步初始化:从存储读取,带防抖
const ensureInitialized = async () => {
if (singleton!.initialized) return
if (singleton!.initializing) {
await singleton!.initializing
return
}
singleton!.initializing = (async () => {
try {
const saved = await getPreference<BasicSubMode>(
UI_SETTINGS_KEYS.BASIC_SUB_MODE,
'system'
)
singleton!.mode.value = (saved === 'system' || saved === 'user')
? saved
: 'system'
console.log(`[useBasicSubMode] 初始化完成,当前值: ${singleton!.mode.value}`)
if (saved !== 'system' && saved !== 'user') {
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
console.log('[useBasicSubMode] 首次初始化,已持久化默认值: system')
}
} catch (e) {
console.error('[useBasicSubMode] 初始化失败,使用默认值 system:', e)
try {
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
} catch {
// 忽略设置失败错误
}
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
// 自动持久化:每次切换自动保存
const setBasicSubMode = async (mode: BasicSubMode) => {
await ensureInitialized()
singleton!.mode.value = mode
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, mode)
console.log(`[useBasicSubMode] 子模式已切换并持久化: ${mode}`)
}
const switchToSystem = () => setBasicSubMode('system')
const switchToUser = () => setBasicSubMode('user')
return {
basicSubMode: readonly(singleton.mode) as Ref<BasicSubMode>,
setBasicSubMode,
switchToSystem,
switchToUser,
ensureInitialized
}
}
```
**关键设计模式**:
1. **单例模式**
```typescript
let singleton: { mode: Ref<SubMode>, initialized: boolean, initializing: Promise<void> | null } | null = null
```
- 确保全局唯一状态
- 避免多实例冲突
2. **防抖初始化**
```typescript
if (singleton!.initialized) return
if (singleton!.initializing) {
await singleton!.initializing
return
}
```
- 避免重复初始化
- 处理并发调用
3. **只读状态暴露**
```typescript
return {
basicSubMode: readonly(singleton.mode) as Ref<BasicSubMode>,
// ...
}
```
- 防止外部直接修改
- 强制通过setter更新
4. **完善的错误处理**
```typescript
try {
// 读取存储
} catch (e) {
// 回退到默认值
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
```
**其他Composable**:
- `useProSubMode.ts` - 与useBasicSubMode结构相同使用ProSubMode类型
- `useImageSubMode.ts` - 与useBasicSubMode结构相同默认值为'text2image'
---
### 4. App.vue集成
#### 导入和状态初始化
```typescript
import {
useBasicSubMode,
useProSubMode,
useImageSubMode,
// ... 其他导入
} from '@prompt-optimizer/ui'
// 功能模式
const { functionMode, setFunctionMode } = useFunctionMode(services as any)
// 三种功能模式的子模式持久化(独立存储)
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services as any)
const { proSubMode, setProSubMode } = useProSubMode(services as any)
const { imageSubMode, setImageSubMode } = useImageSubMode(services as any)
```
#### 导航栏模板
```vue
<template #core-nav>
<NSpace :size="12" align="center">
<!-- 功能模式选择器 -->
<FunctionModeSelector
:modelValue="functionMode"
@update:modelValue="handleModeSelect"
/>
<!-- 子模式选择器 - 基础模式 -->
<OptimizationModeSelectorUI
v-if="functionMode === 'basic'"
:modelValue="basicSubMode"
@change="handleBasicSubModeChange"
/>
<!-- 子模式选择器 - 上下文模式 -->
<OptimizationModeSelectorUI
v-if="functionMode === 'pro'"
:modelValue="proSubMode"
@change="handleProSubModeChange"
/>
<!-- 子模式选择器 - 图像模式 -->
<ImageModeSelector
v-if="functionMode === 'image'"
:modelValue="imageSubMode"
@change="handleImageSubModeChange"
/>
</NSpace>
</template>
```
#### 应用启动初始化
```typescript
onMounted(async () => {
// ... 其他初始化代码 ...
// 根据当前功能模式,从存储恢复对应的子模式选择
if (functionMode.value === "basic") {
const { ensureInitialized } = useBasicSubMode(services as any);
await ensureInitialized();
selectedOptimizationMode.value = basicSubMode.value as OptimizationMode;
console.log(`[App] 基础模式子模式已恢复: ${basicSubMode.value}`);
} else if (functionMode.value === "pro") {
const { ensureInitialized } = useProSubMode(services as any);
await ensureInitialized();
selectedOptimizationMode.value = proSubMode.value as OptimizationMode;
await handleContextModeChange(
proSubMode.value as import("@prompt-optimizer/core").ContextMode,
);
console.log(`[App] 上下文模式子模式已恢复: ${proSubMode.value}`);
} else if (functionMode.value === "image") {
const { ensureInitialized } = useImageSubMode(services as any);
await ensureInitialized();
console.log(`[App] 图像模式子模式已恢复: ${imageSubMode.value}`);
}
})
```
#### 子模式切换处理
```typescript
// 基础模式子模式变更处理器
const handleBasicSubModeChange = async (mode: OptimizationMode) => {
await setBasicSubMode(mode as import("@prompt-optimizer/core").BasicSubMode);
selectedOptimizationMode.value = mode;
console.log(`[App] 基础模式子模式已切换并持久化: ${mode}`);
};
// 上下文模式子模式变更处理器
const handleProSubModeChange = async (mode: OptimizationMode) => {
await setProSubMode(mode as import("@prompt-optimizer/core").ProSubMode);
selectedOptimizationMode.value = mode;
if (services.value?.contextMode.value !== mode) {
await handleContextModeChange(
mode as import("@prompt-optimizer/core").ContextMode,
);
}
console.log(`[App] 上下文模式子模式已切换并持久化: ${mode}`);
};
// 图像模式子模式变更处理器
const handleImageSubModeChange = async (mode: import("@prompt-optimizer/core").ImageSubMode) => {
await setImageSubMode(mode);
console.log(`[App] 图像模式子模式已切换并持久化: ${mode}`);
// 通知 ImageWorkspace 更新
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("image-submode-changed", {
detail: { mode }
}));
}
};
```
---
### 5. 图像模式特殊处理
#### ImageWorkspace.vue 修改
**移除内部选择器**:
```vue
<!-- ❌ 移除前 -->
<ImageModeSelector v-model="imageMode" @change="handleImageModeChange" />
<!-- ✅ 移除后 -->
<!-- 图像模式选择器已移到导航栏 -->
```
**监听导航栏事件**:
```typescript
// 图像子模式变更事件处理器
const handleImageSubModeChanged = (e: CustomEvent) => {
const { mode } = e.detail
if (mode && mode !== imageMode.value) {
console.log(`[ImageWorkspace] 接收到导航栏子模式切换事件: ${mode}`)
handleImageModeChange(mode)
}
}
onMounted(() => {
window.addEventListener(
"image-submode-changed",
handleImageSubModeChanged as EventListener,
);
})
onBeforeUnmount(() => {
window.removeEventListener(
"image-submode-changed",
handleImageSubModeChanged as EventListener,
);
})
```
#### useImageWorkspace.ts 修复
**问题**: 初始化时未从存储恢复 `imageMode`
**修复**:
```typescript
// 文件: packages/ui/src/composables/useImageWorkspace.ts
// 1. 导入 UI_SETTINGS_KEYS
import {
IMAGE_MODE_KEYS,
UI_SETTINGS_KEYS, // ✅ 新增
// ...
} from '@prompt-optimizer/core'
// 2. 修改 restoreSelections 方法
const restoreSelections = async () => {
try {
state.selectedTextModelKey = await getPreference(...)
state.selectedImageModelKey = await getPreference(...)
state.isCompareMode = await getPreference(...)
// ✅ 恢复图像子模式(从全局持久化存储读取)
const savedImageMode = await getPreference(
UI_SETTINGS_KEYS.IMAGE_SUB_MODE,
"text2image",
);
if (savedImageMode === "text2image" || savedImageMode === "image2image") {
state.imageMode = savedImageMode;
console.log(`[useImageWorkspace] 图像子模式已从存储恢复: ${savedImageMode}`);
}
await restoreTemplateSelection();
await restoreImageIterateTemplateSelection();
} catch (error) {
console.warn("Failed to restore selections:", error);
}
}
```
---
## 🔄 数据流
### 初始化流程
```
App启动
读取 FUNCTION_MODE → 确定当前功能模式
根据功能模式调用对应的 ensureInitialized()
┌──────────┬──────────┬──────────┐
│ basic │ pro │ image │
│ ↓ │ ↓ │ ↓ │
│ BASIC_ │ PRO_ │ IMAGE_ │
│ SUB_MODE │ SUB_MODE │ SUB_MODE │
└──────────┴──────────┴──────────┘
恢复子模式状态 → 显示对应的选择器
```
### 切换流程
```
用户点击导航栏选择器
触发 onChange 事件
调用对应的 handleSubModeChange
调用 setSubMode(newMode)
更新内存状态 → 保存到 localStorage
触发响应式更新 → UI自动刷新
(图像模式)发送自定义事件通知 ImageWorkspace
```
---
## 📝 关键代码位置
| 功能 | 文件路径 | 行号范围 |
|------|----------|----------|
| 存储键定义 | `packages/core/src/constants/storage-keys.ts` | ~28-32 |
| 类型定义 | `packages/core/src/services/prompt/types.ts` | ~15-25 |
| useBasicSubMode | `packages/ui/src/composables/useBasicSubMode.ts` | 全文 |
| useProSubMode | `packages/ui/src/composables/useProSubMode.ts` | 全文 |
| useImageSubMode | `packages/ui/src/composables/useImageSubMode.ts` | 全文 |
| App.vue 导航栏 | `packages/web/src/App.vue` | ~21-49 |
| App.vue 初始化 | `packages/web/src/App.vue` | ~1566-1586 |
| App.vue 切换器 | `packages/web/src/App.vue` | ~1788-1831 |
| ImageWorkspace 事件 | `packages/ui/src/components/image-mode/ImageWorkspace.vue` | ~1441-1547 |
| useImageWorkspace 修复 | `packages/ui/src/composables/useImageWorkspace.ts` | ~282-292 |
---
## 🧪 测试验证
### 测试场景
#### 场景1: 基础模式持久化
1. 切换到基础模式
2. 选择"用户提示词优化"
3. 刷新页面
4. ✅ 验证: 基础模式仍显示"用户提示词优化"
#### 场景2: 独立性验证
1. 基础模式选择"用户提示词优化"
2. 切换到上下文模式,选择"系统提示词优化"
3. 切换回基础模式
4. ✅ 验证: 基础模式仍显示"用户提示词优化"(证明独立)
#### 场景3: 图像模式初始化修复
1. 切换到图像模式
2. 选择"图生图"
3. 刷新页面
4. ✅ 验证: 文件上传按钮正确显示
### 验证日志
成功的日志输出示例:
```
[useBasicSubMode] 初始化完成,当前值: user
[App] 基础模式子模式已恢复: user
[useProSubMode] 初始化完成,当前值: system
[App] 上下文模式子模式已恢复: system
[useImageSubMode] 初始化完成,当前值: image2image
[useImageWorkspace] 图像子模式已从存储恢复: image2image
[App] 图像模式子模式已恢复: image2image
```
---
## 🎯 实施总结
### 核心成就
1. ✅ 三个功能模式完全独立的子模式管理
2. ✅ 统一的导航栏UI体验
3. ✅ 完善的持久化和恢复机制
4. ✅ 修复了图像模式的初始化问题
### 技术亮点
1. **单例模式**: 确保全局唯一状态
2. **异步初始化**: 不阻塞应用启动
3. **自动持久化**: 用户无感知的状态保存
4. **完善的错误处理**: 回退机制保证可用性
5. **清晰的日志**: 便于调试和问题排查
### 代码质量
- **类型安全**: 完整的TypeScript类型定义
- **可维护性**: 清晰的职责分离和模块化
- **可扩展性**: 易于添加新的功能模式
- **向后兼容**: 与现有代码平滑集成
---
**文档版本**: v1.0
**最后更新**: 2025-10-22

View File

@@ -75,6 +75,13 @@
- 性能优化:构建体积减少、渲染性能提升
- 为UI框架迁移建立了完整的方法论和最佳实践
### 状态管理系统
- **[126-submode-persistence](./126-submode-persistence/)** - 子模式持久化功能 💾
- 实现三大功能模式(基础/上下文/图像)的独立子模式状态持久化
- 解决状态隔离、跨页面同步和双层状态一致性问题
- 修复图像模式刷新后文件上传按钮不显示的bug
- 建立完整的状态管理最佳实践和设计模式
## 🔧 问题修复系列
### 存储与数据
@@ -142,35 +149,40 @@
- **代码清理和重构** → 121-context-editor-refactor
- **跨平台兼容性问题** → 122-naive-ui-migration
- **性能优化问题** → 122-naive-ui-migration
- **状态持久化问题** → 126-submode-persistence
- **状态同步问题** → 126-submode-persistence
- **刷新后状态丢失** → 126-submode-persistence
### 按技术栈查找
- **Electron相关** → 103, 110, 111, 112, 114
- **Vue/前端相关** → 102, 104, 105, 107, 108, 109, 121, 122
- **Vue/前端相关** → 102, 104, 105, 107, 108, 109, 121, 122, 126
- **UI库相关** → 109, 122
- **浏览器扩展相关** → 119, 122
- **架构设计相关** → 101, 102, 103, 111, 113, 121
- **架构设计相关** → 101, 102, 103, 111, 113, 121, 126
- **服务层相关** → 101, 106, 113, 119
- **IPC通信相关** → 103, 111, 112
- **模板系统相关** → 106, 119
- **组件重构相关** → 107, 121
- **主题系统相关** → 109, 122
- **性能优化相关** → 122
- **状态管理相关** → 126
### 按开发阶段查找
- **项目初期架构** → 101, 102, 103
- **功能开发阶段** → 104, 105, 106, 107
- **功能开发阶段** → 104, 105, 106, 107, 126
- **优化改进阶段** → 108, 109, 121, 122
- **问题修复阶段** → 110, 111, 112, 114, 119
- **问题修复阶段** → 110, 111, 112, 114, 119, 126
- **重构完善阶段** → 113, 121, 122
### 按经验类型查找
- **架构设计经验** → 101, 102, 103, 111, 121
- **功能开发经验** → 106, 107
- **架构设计经验** → 101, 102, 103, 111, 121, 126
- **功能开发经验** → 106, 107, 126
- **UI/UX设计经验** → 108, 109, 122
- **UI框架迁移经验** → 122
- **问题排查经验** → 110, 112, 114, 119
- **问题排查经验** → 110, 112, 114, 119, 126
- **重构实践经验** → 101, 113, 121, 122
- **性能优化经验** → 122
- **状态管理经验** → 126
## 📖 使用建议

View File

@@ -58,6 +58,9 @@
### UI优化系列 (已完成)
- [124-navigation-optimization](./124-navigation-optimization/) - 导航栏优化项目 ✅
### 状态管理系列 (已完成)
- [126-submode-persistence](./126-submode-persistence/) - 子模式持久化功能 ✅
## 📋 文档结构
每个功能点目录包含:
@@ -84,9 +87,10 @@
- **问题修复系列**110, 111, 112
- **服务重构系列**113
- **UI优化系列**124
- **状态管理系列**126
### 按状态查找
- **已完成**101, 102, 103, 108, 109, 110, 111, 112, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124
- **已完成**101, 102, 103, 108, 109, 110, 111, 112, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 126
- **进行中**106, 107, 113
- **计划中**104, 105
@@ -125,8 +129,8 @@
## 📊 统计信息
- **总归档数**: 24
- **已完成**: 19
- **总归档数**: 25
- **已完成**: 20
- **进行中**: 3
- **计划中**: 2
- **下一个编号**: 125
- **下一个编号**: 127

View File

@@ -1,143 +0,0 @@
# 图像模式模板与迭代能力改造规范Spec
本文档定义在 UI 与 Core 中新增“图像模式”的三类模板类型与相关能力,使图像模式与基础模式在体验上对齐(不启用高级模式),并为图像场景提供专属的“迭代”模板与流程。
- 目标
- 在核心模板体系中引入独立的 `imageIterate` 模板类型,避免通过 tags/id 的二次过滤。
- 按文生图/图生图/图像迭代三类划分默认模板目录与加载逻辑。
- 在模板管理界面添加图像三类的管理入口(与基础/上下文并列)。
- 在图像模式工作区完善“迭代”模板的初始化、持久化与调用(无需高级模式)。
- 范围
- Core模板类型、默认模板、加载器与管理器 API 扩展。
- UI模板管理器新增标签、TemplateSelect 支持 `imageIterate`、图像工作区迭代逻辑完善。
- 存储:图像模式专属的迭代模板选择键。
- i18n图像模板分类的界面文案。
## 一、Core 改造
### 1. 新增模板类型 `imageIterate`
- 文件:`packages/core/src/services/template/types.ts`
-`TemplateMetadata.templateType` union 与 zod schema 中新增 `'imageIterate'`
-`ITemplateManager.listTemplatesByType` 的参数 union 中新增 `'imageIterate'`
- 文件:`packages/core/src/services/template/manager.ts`
- `listTemplatesByType` 参数 union 同步扩展(实现已按模板元数据类型过滤,兼容新增类型)。
- 文件:`packages/core/src/services/template/electron-proxy.ts`
- 接口定义中的 union 同步增加 `'imageIterate'`
- 文件:`packages/core/src/services/template/static-loader.ts`
- `TemplateType` union 新增 `'imageIterate'`
- `byType` 初始化新增 `imageIterate` 节点。
-`metadata.templateType === 'imageIterate'` 规范化映射到 `normalizedType = 'imageIterate'`
- 日志统计中增加 `imageIterate` 的计数。
- 文件:`packages/core/src/services/prompt/service.ts`
- `getDefaultTemplateId(...)``'imageIterate'` 增加处理与回退(若无可用 `imageIterate`,回退至 `iterate`)。
### 2. 默认模板目录重构与新增
- 目录:`packages/core/src/services/template/default-templates/image-optimize/`
- `text2image/`
- 存放文生图优化模板:`*-optimize(.ts|_en.ts)` (如 `dalle-optimize` / `general-image-optimize` / `chinese-model-optimize` / `stable-diffusion-optimize`)。
- `image2image/`
- 存放图生图优化模板:`image2image-optimize(.ts|_en.ts)`
- `iterate/`(新增)
- 新增图像迭代模板(中/英):
- `image-iterate-general.ts`
- `image-iterate-general_en.ts`
- 模板元数据:`templateType: 'imageIterate'``language: 'zh'|'en'``version``lastModified``author``description`
- 内容要求(建议为高级模板:消息数组):
- System描述“基于优化后图像提示词做定向改进”的规则强调视觉意图/风格延续、构图/光照/风格参数、强度/seed 等可控性。
- User包含 `originalPrompt`(或 last optimized prompt`iterateInput`,指示“直接输出新的优化图像提示词”。
- 文件:`packages/core/src/services/template/default-templates/index.ts`
- 更新 import 路径(目录重构后)。
- 引入并聚合 `image-iterate-general(.ts|_en.ts)``ALL_TEMPLATES`
## 二、UI 改造
### 1. 模板管理器TemplateManager.vue
- 文件:`packages/ui/src/components/TemplateManager.vue`
- 新增三枚类别按钮(与现有两行三列布局一致):
- 图像 · 文生图 → `currentCategory='image-text2image-optimize'` → 类型 `'text2imageOptimize'`
- 图像 · 图生图 → `currentCategory='image-image2image-optimize'` → 类型 `'image2imageOptimize'`
- 图像 · 迭代 → `currentCategory='image-iterate'` → 类型 `'imageIterate'`
- 类型映射:
- props 支持初始 `templateType: 'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate'`
- `getCategoryFromProps()` 增加以上映射。
- `getCurrentTemplateType()` 返回当前分类对应的元数据类型(严格等值)。
- 列表过滤:
- `filteredTemplates` 新增三类过滤条件:`text2imageOptimize` / `image2imageOptimize` / `imageIterate`
- i18n
-`zh-CN.ts` / `en-US.ts` 新增:
- `templateManager.imageText2ImageTemplates`
- `templateManager.imageImage2ImageTemplates`
- `templateManager.imageIterateTemplates`
### 2. 模板选择器TemplateSelect.vue
- 文件:`packages/ui/src/components/TemplateSelect.vue`
- props.validator 支持 `'imageIterate'`
- 其余无需变更,`listTemplatesByType(props.type)` 会加载 `imageIterate`
## 三、图像模式工作区useImageWorkspace
### 1. 存储键Storage Keys
- 文件:`packages/core/src/constants/storage-keys.ts`
-`IMAGE_MODE_KEYS` 中新增:
- `SELECTED_ITERATE_TEMPLATE: 'app:image-mode:selected-iterate-template'`
### 2. 迭代模板的初始化、恢复与保存
- 文件:`packages/ui/src/composables/useImageWorkspace.ts`
-`initialize()/restoreSelections()` 阶段:
- 读取 `IMAGE_MODE_KEYS.SELECTED_ITERATE_TEMPLATE`;若为空,则 `listTemplatesByType('imageIterate')` 并选择第一个可用模板。
-`saveSelections()` 中:
- 若存在 `selectedIterateTemplate`,保存其 `id` 至上述存储键。
### 3. 触发迭代时使用所选模板
- 与 PromptPanel 的迭代对接:
- 推荐绑定到现有 `handleIteratePrompt`(其签名包含 `templateId`),或在 `handleIterate` 内部调用 `iteratePromptStream` 时明确传入 `state.selectedIterateTemplate.id`
- 若未选择toast 明确提示“请选择图像迭代模板”。
### 4. 打开模板管理器类型(修复与支持)
- 文件:`packages/ui/src/composables/useImageWorkspace.ts`
- 修正 `handleOpenTemplateManager` 的优先级与类型选择:
- 传入 `'text2imageOptimize' | 'image2imageOptimize' | 'imageIterate'`
- 未传入参数时,根据 `imageMode` 推断 `'text2imageOptimize' | 'image2imageOptimize'`
## 四、验收标准Acceptance Criteria
- Core
- TS 编译通过,`imageIterate` 类型在各接口与实现中受支持。
- 静态加载器日志显示 `imageIterate` 模板数量 > 0默认模板存在
- `TemplateManager.listTemplatesByType('imageIterate')` 返回新增默认模板。
- UI
- 模板管理器出现 9 个分类(基础 3 + 上下文 3 + 图像 3
- 每个“图像”分类仅显示相应类型的模板。
- TemplateSelect 可传入 `'imageIterate'` 并正确加载列表。
- 图像工作区
- 首次进入时,若无保存记录,自动选择一个 `imageIterate` 默认模板。
- 点击“继续优化”(图像模式)能直接弹出迭代输入弹窗,不再提示“请选择迭代模板”。
- 从图像工作区打开“管理模板”,能够跳转到正确的图像分类。
## 五、实施计划Rollout Plan
1) Phase 1 — Core
- 新增 `imageIterate` 类型types/manager/proxy/static-loader/service
- 重构默认模板目录,并新增 `image-iterate`(中/英)。
- 更新 `default-templates/index.ts` 聚合导入。
2) Phase 2 — UI
- TemplateManager 新增图像三类按钮与过滤。
- TemplateSelect 支持 `'imageIterate'`
- i18n 新增相关文案。
3) Phase 3 — Image Workspace
- 增加/恢复 `selectedIterateTemplate` 逻辑与存储。
- 迭代调用传递所选模板 `id`;修复打开模板管理器类型。
4) Phase 4 — 验证
- 编译 core/ui 包,确保类型与构建通过。
- 验证模板列表、选择与迭代弹窗的端到端流程。
- 回归基础/上下文模板管理无回归。
## 六、风险与对策
- 类型遗漏:通过全局检索 `listTemplatesByType(...)``templateType` union 的使用点保证全部覆盖CI 构建验证。
- 兼容性:旧数据若仅保存 `iterate`,恢复时提示用户切换至 `imageIterate`;必要时提供回退逻辑(仅临时)。
- UI 对接:图像工作区与全局 TemplateManager 的控制方式需统一(建议通过 provide/inject 或复用 `useTemplateManager`)。
---
如需补充更多内置模板(如“风格保持迭代”“细节增强迭代”等),可在 `image-optimize/iterate/` 下继续添加,并在 `index.ts` 中聚合导出。

View File

@@ -7,46 +7,52 @@
// 核心服务存储键
export const CORE_SERVICE_KEYS = {
MODELS: 'models', // 模型配置存储键
IMAGE_MODELS: 'image-models', // 图像模型配置存储键
USER_TEMPLATES: 'user-templates', // 用户模板存储键
PROMPT_HISTORY: 'prompt_history', // 提示词历史记录存储键
} as const
MODELS: "models", // 模型配置存储键
IMAGE_MODELS: "image-models", // 图像模型配置存储键
USER_TEMPLATES: "user-templates", // 用户模板存储键
PROMPT_HISTORY: "prompt_history", // 提示词历史记录存储键
} as const;
// UI设置相关
export const UI_SETTINGS_KEYS = {
THEME_ID: 'app:settings:ui:theme-id',
PREFERRED_LANGUAGE: 'app:settings:ui:preferred-language',
BUILTIN_TEMPLATE_LANGUAGE: 'app:settings:ui:builtin-template-language',
FUNCTION_MODE: 'app:settings:ui:function-mode',
} as const
THEME_ID: "app:settings:ui:theme-id",
PREFERRED_LANGUAGE: "app:settings:ui:preferred-language",
BUILTIN_TEMPLATE_LANGUAGE: "app:settings:ui:builtin-template-language",
FUNCTION_MODE: "app:settings:ui:function-mode",
// 子模式持久化(三种功能模式独立存储)
BASIC_SUB_MODE: "app:settings:ui:basic-sub-mode", // 基础模式的子模式system/user
PRO_SUB_MODE: "app:settings:ui:pro-sub-mode", // 上下文模式的子模式system/user
IMAGE_SUB_MODE: "app:settings:ui:image-sub-mode", // 图像模式的子模式text2image/image2image
} as const;
// 模型选择相关
export const MODEL_SELECTION_KEYS = {
OPTIMIZE_MODEL: 'app:selected-optimize-model',
TEST_MODEL: 'app:selected-test-model',
} as const
OPTIMIZE_MODEL: "app:selected-optimize-model",
TEST_MODEL: "app:selected-test-model",
} as const;
// 模板选择相关
export const TEMPLATE_SELECTION_KEYS = {
SYSTEM_OPTIMIZE_TEMPLATE: 'app:selected-optimize-template', // 系统优化模板(兼容旧版本)
USER_OPTIMIZE_TEMPLATE: 'app:selected-user-optimize-template', // 用户优化模板
ITERATE_TEMPLATE: 'app:selected-iterate-template', // 迭代模板
CONTEXT_SYSTEM_OPTIMIZE_TEMPLATE: 'app:selected-context-system-optimize-template',
CONTEXT_USER_OPTIMIZE_TEMPLATE: 'app:selected-context-user-optimize-template',
CONTEXT_ITERATE_TEMPLATE: 'app:selected-context-iterate-template',
} as const
SYSTEM_OPTIMIZE_TEMPLATE: "app:selected-optimize-template", // 系统优化模板(兼容旧版本)
USER_OPTIMIZE_TEMPLATE: "app:selected-user-optimize-template", // 用户优化模板
ITERATE_TEMPLATE: "app:selected-iterate-template", // 迭代模板
CONTEXT_SYSTEM_OPTIMIZE_TEMPLATE:
"app:selected-context-system-optimize-template",
CONTEXT_USER_OPTIMIZE_TEMPLATE: "app:selected-context-user-optimize-template",
CONTEXT_ITERATE_TEMPLATE: "app:selected-context-iterate-template",
} as const;
// 图像模式选择相关
export const IMAGE_MODE_KEYS = {
SELECTED_TEXT_MODEL: 'app:image-mode:selected-text-model',
SELECTED_IMAGE_MODEL: 'app:image-mode:selected-image-model',
SELECTED_TEXT_MODEL: "app:image-mode:selected-text-model",
SELECTED_IMAGE_MODEL: "app:image-mode:selected-image-model",
// 按模式分别存储模板选择
SELECTED_TEMPLATE_TEXT2IMAGE: 'app:image-mode:selected-template:text2image',
SELECTED_TEMPLATE_IMAGE2IMAGE: 'app:image-mode:selected-template:image2image',
SELECTED_ITERATE_TEMPLATE: 'app:image-mode:selected-iterate-template',
COMPARE_MODE_ENABLED: 'app:image-mode:compare-mode-enabled',
} as const
SELECTED_TEMPLATE_TEXT2IMAGE: "app:image-mode:selected-template:text2image",
SELECTED_TEMPLATE_IMAGE2IMAGE: "app:image-mode:selected-template:image2image",
SELECTED_ITERATE_TEMPLATE: "app:image-mode:selected-iterate-template",
COMPARE_MODE_ENABLED: "app:image-mode:compare-mode-enabled",
} as const;
// 所有存储键的联合类型
export const ALL_STORAGE_KEYS = {
@@ -55,15 +61,21 @@ export const ALL_STORAGE_KEYS = {
...MODEL_SELECTION_KEYS,
...TEMPLATE_SELECTION_KEYS,
...IMAGE_MODE_KEYS,
} as const
} as const;
// 导出所有键的数组用于DataManager等需要遍历的场景
export const ALL_STORAGE_KEYS_ARRAY = Object.values(ALL_STORAGE_KEYS)
export const ALL_STORAGE_KEYS_ARRAY = Object.values(ALL_STORAGE_KEYS);
// 类型定义
export type CoreServiceKey = typeof CORE_SERVICE_KEYS[keyof typeof CORE_SERVICE_KEYS]
export type UISettingsKey = typeof UI_SETTINGS_KEYS[keyof typeof UI_SETTINGS_KEYS]
export type ModelSelectionKey = typeof MODEL_SELECTION_KEYS[keyof typeof MODEL_SELECTION_KEYS]
export type TemplateSelectionKey = typeof TEMPLATE_SELECTION_KEYS[keyof typeof TEMPLATE_SELECTION_KEYS]
export type ImageModeKey = typeof IMAGE_MODE_KEYS[keyof typeof IMAGE_MODE_KEYS]
export type StorageKey = typeof ALL_STORAGE_KEYS[keyof typeof ALL_STORAGE_KEYS]
export type CoreServiceKey =
(typeof CORE_SERVICE_KEYS)[keyof typeof CORE_SERVICE_KEYS];
export type UISettingsKey =
(typeof UI_SETTINGS_KEYS)[keyof typeof UI_SETTINGS_KEYS];
export type ModelSelectionKey =
(typeof MODEL_SELECTION_KEYS)[keyof typeof MODEL_SELECTION_KEYS];
export type TemplateSelectionKey =
(typeof TEMPLATE_SELECTION_KEYS)[keyof typeof TEMPLATE_SELECTION_KEYS];
export type ImageModeKey =
(typeof IMAGE_MODE_KEYS)[keyof typeof IMAGE_MODE_KEYS];
export type StorageKey =
(typeof ALL_STORAGE_KEYS)[keyof typeof ALL_STORAGE_KEYS];

View File

@@ -1,12 +1,12 @@
import { PromptRecord } from '../history/types';
import { StreamHandlers } from '../llm/types';
import { PromptRecord } from "../history/types";
import { StreamHandlers } from "../llm/types";
/**
* 工具调用相关类型
*/
export interface ToolCall {
id: string;
type: 'function';
type: "function";
function: {
name: string;
arguments: string;
@@ -20,7 +20,7 @@ export interface FunctionDefinition {
}
export interface ToolDefinition {
type: 'function';
type: "function";
function: FunctionDefinition;
}
@@ -28,8 +28,8 @@ export interface ToolDefinition {
* 统一的消息结构
*/
export interface ConversationMessage {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string; // 可包含变量语法 {{variableName}}
role: "system" | "user" | "assistant" | "tool";
content: string; // 可包含变量语法 {{variableName}}
/**
* 函数调用名称assistant消息
*/
@@ -48,23 +48,31 @@ export interface ConversationMessage {
* 优化模式枚举
* 用于区分不同的提示词优化类型
*/
export type OptimizationMode = 'system' | 'user';
export type OptimizationMode = "system" | "user";
/**
* 子模式类型定义(三种功能模式独立)
* 用于持久化各功能模式下的子模式选择
*/
export type BasicSubMode = "system" | "user"; // 基础模式
export type ProSubMode = "system" | "user"; // 上下文模式
export type ImageSubMode = "text2image" | "image2image"; // 图像模式
/**
* 优化请求接口
*/
export interface OptimizationRequest {
optimizationMode: OptimizationMode;
targetPrompt: string; // 待优化的提示词
targetPrompt: string; // 待优化的提示词
templateId?: string;
modelKey: string;
// 🆕 上下文模式(用于变量替换策略)
contextMode?: import('../context/types').ContextMode;
contextMode?: import("../context/types").ContextMode;
// 新增:高级模式上下文(可选,保持向后兼容)
advancedContext?: {
variables?: Record<string, string>; // 自定义变量
messages?: ConversationMessage[]; // 自定义会话消息
tools?: ToolDefinition[]; // 🆕 工具定义支持
variables?: Record<string, string>; // 自定义变量
messages?: ConversationMessage[]; // 自定义会话消息
tools?: ToolDefinition[]; // 🆕 工具定义支持
};
}
@@ -73,11 +81,11 @@ export interface OptimizationRequest {
*/
export interface CustomConversationRequest {
modelKey: string;
messages: ConversationMessage[]; // 使用相同的消息结构
variables: Record<string, string>; // 包含预定义+自定义变量
tools?: ToolDefinition[]; // 🆕 工具定义支持
messages: ConversationMessage[]; // 使用相同的消息结构
variables: Record<string, string>; // 包含预定义+自定义变量
tools?: ToolDefinition[]; // 🆕 工具定义支持
// 🆕 上下文模式(用于变量替换策略)
contextMode?: import('../context/types').ContextMode;
contextMode?: import("../context/types").ContextMode;
}
/**
@@ -86,33 +94,33 @@ export interface CustomConversationRequest {
export interface IPromptService {
/** 优化提示词 - 支持提示词类型和增强功能 */
optimizePrompt(request: OptimizationRequest): Promise<string>;
/** 迭代优化提示词 */
iteratePrompt(
originalPrompt: string,
lastOptimizedPrompt: string,
iterateInput: string,
modelKey: string,
templateId?: string
templateId?: string,
): Promise<string>;
/** 测试提示词 - 支持可选系统提示词 */
testPrompt(
systemPrompt: string,
userPrompt: string,
modelKey: string
modelKey: string,
): Promise<string>;
/** 获取历史记录 */
getHistory(): Promise<PromptRecord[]>;
/** 获取迭代链 */
getIterationChain(recordId: string): Promise<PromptRecord[]>;
/** 优化提示词(流式)- 支持提示词类型和增强功能 */
optimizePromptStream(
request: OptimizationRequest,
callbacks: StreamHandlers
callbacks: StreamHandlers,
): Promise<void>;
/** 迭代优化提示词(流式) */
@@ -122,7 +130,7 @@ export interface IPromptService {
iterateInput: string,
modelKey: string,
handlers: StreamHandlers,
templateId: string
templateId: string,
): Promise<void>;
/** 测试提示词(流式)- 支持可选系统提示词 */
@@ -130,14 +138,14 @@ export interface IPromptService {
systemPrompt: string,
userPrompt: string,
modelKey: string,
callbacks: StreamHandlers
callbacks: StreamHandlers,
): Promise<void>;
/** 自定义会话测试(流式)- 高级模式功能 */
testCustomConversationStream(
request: CustomConversationRequest,
callbacks: StreamHandlers
callbacks: StreamHandlers,
): Promise<void>;
}
export type { StreamHandlers };
export type { StreamHandlers };

View File

@@ -1,158 +1,203 @@
<!-- 输入面板组件 - 纯Naive UI实现 -->
<template>
<NSpace vertical :size="16"
>
<!-- 标题区域 -->
<NFlex justify="space-between" align="center" :wrap="false">
<NText :depth="1" style="font-size: 18px; font-weight: 500;">{{ label }}</NText>
<NFlex align="center" :size="12">
<slot name="optimization-mode-selector"></slot>
<!-- 预览按钮 -->
<NButton
v-if="showPreview"
type="tertiary"
size="small"
@click="$emit('open-preview')"
:title="$t('common.preview')"
ghost
round
>
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</NIcon>
</template>
</NButton>
<!-- 全屏按钮 -->
<NButton
type="tertiary"
size="small"
@click="openFullscreen"
:title="$t('common.expand')"
ghost
round
>
<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="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</NIcon>
</template>
</NButton>
</NFlex>
</NFlex>
<NSpace vertical :size="16">
<!-- 标题区域 -->
<NFlex justify="space-between" align="center" :wrap="false">
<NText :depth="1" style="font-size: 18px; font-weight: 500">{{
label
}}</NText>
<NFlex align="center" :size="12">
<!-- 预览按钮 -->
<NButton
v-if="showPreview"
type="tertiary"
size="small"
@click="$emit('open-preview')"
:title="$t('common.preview')"
ghost
round
>
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
</NIcon>
</template>
</NButton>
<!-- 全屏按钮 -->
<NButton
type="tertiary"
size="small"
@click="openFullscreen"
:title="$t('common.expand')"
ghost
round
>
<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="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/>
</svg>
</NIcon>
</template>
</NButton>
</NFlex>
</NFlex>
<!-- 输入框 -->
<NInput
:value="modelValue"
@update:value="$emit('update:modelValue', $event)"
type="textarea"
:placeholder="placeholder"
:rows="4"
:autosize="{ minRows: 4, maxRows: 12 }"
clearable
show-count
/>
<!-- 输入框 -->
<NInput
:value="modelValue"
@update:value="$emit('update:modelValue', $event)"
type="textarea"
:placeholder="placeholder"
:rows="4"
:autosize="{ minRows: 4, maxRows: 12 }"
clearable
show-count
/>
<!-- 控制面板 -->
<NGrid :cols="24" :x-gap="12" responsive="screen">
<!-- 模型选择 -->
<NGridItem :span="6" :xs="24" :sm="6">
<NSpace vertical :size="8">
<NText :depth="2" style="font-size: 14px; font-weight: 500;">{{ modelLabel }}</NText>
<slot name="model-select"></slot>
</NSpace>
</NGridItem>
<!-- 控制面板 -->
<NGrid :cols="24" :x-gap="12" responsive="screen">
<!-- 模型选择 -->
<NGridItem :span="6" :xs="24" :sm="6">
<NSpace vertical :size="8">
<NText
:depth="2"
style="font-size: 14px; font-weight: 500"
>{{ modelLabel }}</NText
>
<slot name="model-select"></slot>
</NSpace>
</NGridItem>
<!-- 提示词模板选择 -->
<NGridItem v-if="templateLabel" :span="11" :xs="24" :sm="11">
<NSpace vertical :size="8">
<NText :depth="2" style="font-size: 14px; font-weight: 500;">{{ templateLabel }}</NText>
<slot name="template-select"></slot>
</NSpace>
</NGridItem>
<!-- 提示词模板选择 -->
<NGridItem v-if="templateLabel" :span="11" :xs="24" :sm="11">
<NSpace vertical :size="8">
<NText
:depth="2"
style="font-size: 14px; font-weight: 500"
>{{ templateLabel }}</NText
>
<slot name="template-select"></slot>
</NSpace>
</NGridItem>
<!-- 控制按钮组 -->
<NGridItem :span="templateLabel ? 2 : 13" :xs="24" :sm="templateLabel ? 2 : 13">
<NSpace vertical :size="8" align="end">
<slot name="control-buttons"></slot>
</NSpace>
</NGridItem>
<!-- 控制按钮组 -->
<NGridItem
:span="templateLabel ? 2 : 13"
:xs="24"
:sm="templateLabel ? 2 : 13"
>
<NSpace vertical :size="8" align="end">
<slot name="control-buttons"></slot>
</NSpace>
</NGridItem>
<!-- 提交按钮 -->
<NGridItem :span="5" :xs="24" :sm="5">
<NSpace vertical :size="8" align="end">
<NButton
type="primary"
size="medium"
@click="$emit('submit')"
:loading="loading"
:disabled="loading || disabled || !modelValue.trim()"
block
>
{{ loading ? loadingText : buttonText }}
</NButton>
</NSpace>
</NGridItem>
</NGrid>
</NSpace>
<!-- 全屏弹窗 -->
<FullscreenDialog v-model="isFullscreen" :title="label">
<NInput
v-model:value="fullscreenValue"
type="textarea"
:placeholder="placeholder"
:autosize="{ minRows: 20 }"
clearable
show-count
/>
</FullscreenDialog>
<!-- 提交按钮 -->
<NGridItem :span="5" :xs="24" :sm="5">
<NSpace vertical :size="8" align="end">
<NButton
type="primary"
size="medium"
@click="$emit('submit')"
:loading="loading"
:disabled="loading || disabled || !modelValue.trim()"
block
>
{{ loading ? loadingText : buttonText }}
</NButton>
</NSpace>
</NGridItem>
</NGrid>
</NSpace>
<!-- 全屏弹窗 -->
<FullscreenDialog v-model="isFullscreen" :title="label">
<NInput
v-model:value="fullscreenValue"
type="textarea"
:placeholder="placeholder"
:autosize="{ minRows: 20 }"
clearable
show-count
/>
</FullscreenDialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NInput, NButton, NText, NSpace, NFlex, NGrid, NGridItem, NIcon } from 'naive-ui'
import { useFullscreen } from '../composables/useFullscreen'
import FullscreenDialog from './FullscreenDialog.vue'
import { computed } from "vue";
import {
NInput,
NButton,
NText,
NSpace,
NFlex,
NGrid,
NGridItem,
NIcon,
} from "naive-ui";
import { useFullscreen } from "../composables/useFullscreen";
import FullscreenDialog from "./FullscreenDialog.vue";
interface Props {
modelValue: string
selectedModel: string
label: string
placeholder?: string
modelLabel: string
templateLabel?: string
buttonText: string
loadingText: string
loading?: boolean
disabled?: boolean
showPreview?: boolean
modelValue: string;
selectedModel: string;
label: string;
placeholder?: string;
modelLabel: string;
templateLabel?: string;
buttonText: string;
loadingText: string;
loading?: boolean;
disabled?: boolean;
showPreview?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '',
templateLabel: '',
loading: false,
disabled: false,
showPreview: false
})
placeholder: "",
templateLabel: "",
loading: false,
disabled: false,
showPreview: false,
});
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:selectedModel': [value: string]
'submit': []
'configModel': []
'open-preview': []
}>()
"update:modelValue": [value: string];
"update:selectedModel": [value: string];
submit: [];
configModel: [];
"open-preview": [];
}>();
// 使用全屏组合函数
const { isFullscreen, fullscreenValue, openFullscreen } = useFullscreen(
computed(() => props.modelValue),
(value) => emit('update:modelValue', value)
)
</script>
computed(() => props.modelValue),
(value) => emit("update:modelValue", value),
);
</script>

View File

@@ -72,9 +72,6 @@
@configModel="emit('config-model')"
@open-preview="emit('open-input-preview')"
>
<template #optimization-mode-selector>
<slot name="optimization-mode-selector"></slot>
</template>
<template #model-select>
<slot name="optimize-model-select"></slot>
</template>
@@ -144,64 +141,113 @@
</NFlex>
<!-- 右侧测试区域 -->
<NCard
<NFlex
vertical
:style="{
flex: 1,
overflow: 'auto',
height: '100%',
gap: '12px',
}"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<TestAreaPanel
:optimization-mode="optimizationMode"
context-mode="system"
:optimized-prompt="optimizedPrompt"
:is-test-running="isTestRunning"
:global-variables="globalVariables"
:context-variables="contextVariables"
:predefined-variables="predefinedVariables"
: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"
@test="emit('test')"
@compare-toggle="emit('compare-toggle')"
@open-variable-manager="emit('open-variable-manager')"
@open-preview="emit('open-test-preview')"
<!-- 测试区域操作栏 -->
<NCard size="small" :style="{ flexShrink: 0 }">
<NFlex justify="space-between" align="center">
<!-- 左侧区域标识 -->
<NFlex align="center" :size="8">
<NText strong>{{ $t("test.areaTitle") }}</NText>
<NTag type="info" size="small">
<template #icon><span></span></template>
{{ $t("contextMode.system.label") }}
</NTag>
</NFlex>
<!-- 右侧快捷操作按钮 -->
<NFlex :size="8">
<NButton
size="small"
quaternary
@click="emit('open-global-variables')"
:title="$t('contextMode.actions.globalVariables')"
>
<template #icon><span>📊</span></template>
<span v-if="!isMobile">{{
$t("contextMode.actions.globalVariables")
}}</span>
</NButton>
<NButton
size="small"
quaternary
@click="emit('open-context-variables')"
:title="$t('contextMode.actions.contextVariables')"
>
<template #icon><span>📝</span></template>
<span v-if="!isMobile">{{
$t("contextMode.actions.contextVariables")
}}</span>
</NButton>
</NFlex>
</NFlex>
</NCard>
<!-- 测试区域主内容 -->
<NCard
:style="{ flex: 1, overflow: 'auto' }"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<!-- 模型选择插槽 -->
<template #model-select>
<slot name="test-model-select"></slot>
</template>
<TestAreaPanel
:optimization-mode="optimizationMode"
context-mode="system"
:optimized-prompt="optimizedPrompt"
:is-test-running="isTestRunning"
:global-variables="globalVariables"
:context-variables="contextVariables"
:predefined-variables="predefinedVariables"
: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"
@test="emit('test')"
@compare-toggle="emit('compare-toggle')"
@open-variable-manager="emit('open-variable-manager')"
@open-preview="emit('open-test-preview')"
>
<!-- 模型选择插槽 -->
<template #model-select>
<slot name="test-model-select"></slot>
</template>
<!-- 结果显示插槽 -->
<template #original-result>
<slot name="original-result"></slot>
</template>
<!-- 结果显示插槽 -->
<template #original-result>
<slot name="original-result"></slot>
</template>
<template #optimized-result>
<slot name="optimized-result"></slot>
</template>
<template #optimized-result>
<slot name="optimized-result"></slot>
</template>
<template #single-result>
<slot name="single-result"></slot>
</template>
</TestAreaPanel>
</NCard>
<template #single-result>
<slot name="single-result"></slot>
</template>
</TestAreaPanel>
</NCard>
</NFlex>
</NFlex>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { NCard, NFlex, NButton } from "naive-ui";
import { NCard, NFlex, NButton, NText, NTag } from "naive-ui";
import { useBreakpoints } from "@vueuse/core";
import InputPanelUI from "../InputPanel.vue";
import PromptPanelUI from "../PromptPanel.vue";
import TestAreaPanel from "../TestAreaPanel.vue";
@@ -209,6 +255,13 @@ import ConversationManager from "./ConversationManager.vue";
import type { OptimizationMode, ConversationMessage } from "../../types";
import type { IServices } from "@prompt-optimizer/core";
// 响应式断点
const breakpoints = useBreakpoints({
mobile: 640,
tablet: 1024,
});
const isMobile = breakpoints.smaller("mobile");
// Props 定义 (移除 contextMode因为固定为 system)
interface Props {
// 核心状态

View File

@@ -82,9 +82,6 @@
@configModel="emit('config-model')"
@open-preview="emit('open-input-preview')"
>
<template #optimization-mode-selector>
<slot name="optimization-mode-selector"></slot>
</template>
<template #model-select>
<slot name="optimize-model-select"></slot>
</template>
@@ -134,71 +131,140 @@
</NFlex>
<!-- 右侧测试区域 -->
<NCard
<NFlex
vertical
:style="{
flex: 1,
overflow: 'auto',
height: '100%',
gap: '12px',
}"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<TestAreaPanel
:optimization-mode="optimizationMode"
context-mode="user"
:optimized-prompt="optimizedPrompt"
:is-test-running="isTestRunning"
:global-variables="globalVariables"
:context-variables="contextVariables"
:predefined-variables="predefinedVariables"
: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"
@test="emit('test')"
@compare-toggle="emit('compare-toggle')"
@open-variable-manager="emit('open-variable-manager')"
@open-preview="emit('open-test-preview')"
@variable-change="emit('variable-change', $event[0], $event[1])"
<!-- 测试区域操作栏 -->
<NCard size="small" :style="{ flexShrink: 0 }">
<NFlex justify="space-between" align="center">
<!-- 左侧区域标识 -->
<NFlex align="center" :size="8">
<NText strong>{{ $t("test.areaTitle") }}</NText>
<NTag type="info" size="small">
<template #icon><span>👤</span></template>
{{ $t("contextMode.user.label") }}
</NTag>
</NFlex>
<!-- 右侧快捷操作按钮 -->
<NFlex :size="8">
<NButton
size="small"
quaternary
@click="emit('open-global-variables')"
:title="$t('contextMode.actions.globalVariables')"
>
<template #icon><span>📊</span></template>
<span v-if="!isMobile">{{
$t("contextMode.actions.globalVariables")
}}</span>
</NButton>
<NButton
size="small"
quaternary
@click="emit('open-context-variables')"
:title="$t('contextMode.actions.contextVariables')"
>
<template #icon><span>📝</span></template>
<span v-if="!isMobile">{{
$t("contextMode.actions.contextVariables")
}}</span>
</NButton>
<NButton
size="small"
quaternary
@click="emit('open-tool-manager')"
:title="$t('contextMode.actions.toolManager')"
>
<template #icon><span>🔧</span></template>
<span v-if="!isMobile">{{
$t("contextMode.actions.toolManager")
}}</span>
</NButton>
</NFlex>
</NFlex>
</NCard>
<!-- 测试区域主内容 -->
<NCard
:style="{ flex: 1, overflow: 'auto' }"
content-style="height: 100%; max-height: 100%; overflow: hidden;"
>
<!-- 模型选择插槽 -->
<template #model-select>
<slot name="test-model-select"></slot>
</template>
<TestAreaPanel
:optimization-mode="optimizationMode"
context-mode="user"
:optimized-prompt="optimizedPrompt"
:is-test-running="isTestRunning"
:global-variables="globalVariables"
:context-variables="contextVariables"
:predefined-variables="predefinedVariables"
: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"
@test="emit('test')"
@compare-toggle="emit('compare-toggle')"
@open-variable-manager="emit('open-variable-manager')"
@open-preview="emit('open-test-preview')"
@variable-change="
emit('variable-change', $event[0], $event[1])
"
>
<!-- 模型选择插槽 -->
<template #model-select>
<slot name="test-model-select"></slot>
</template>
<!-- 结果显示插槽 -->
<template #original-result>
<slot name="original-result"></slot>
</template>
<!-- 结果显示插槽 -->
<template #original-result>
<slot name="original-result"></slot>
</template>
<template #optimized-result>
<slot name="optimized-result"></slot>
</template>
<template #optimized-result>
<slot name="optimized-result"></slot>
</template>
<template #single-result>
<slot name="single-result"></slot>
</template>
</TestAreaPanel>
</NCard>
<template #single-result>
<slot name="single-result"></slot>
</template>
</TestAreaPanel>
</NCard>
</NFlex>
</NFlex>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { NCard, NFlex, NButton } from "naive-ui";
import { NCard, NFlex, NButton, NText, NTag } from "naive-ui";
import { useBreakpoints } from "@vueuse/core";
import InputPanelUI from "../InputPanel.vue";
import PromptPanelUI from "../PromptPanel.vue";
import TestAreaPanel from "../TestAreaPanel.vue";
import type { OptimizationMode } from "../../types";
import type { IServices } from "@prompt-optimizer/core";
// 响应式断点
const breakpoints = useBreakpoints({
mobile: 640,
tablet: 1024,
});
const isMobile = breakpoints.smaller("mobile");
// Props 定义 (移除 contextMode 和 会话管理器相关的 props)
interface Props {
// 核心状态

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,34 @@
export * from './useToast'
export * from './useModals'
export * from './usePromptOptimizer'
export * from './usePromptTester'
export * from './usePromptHistory'
export * from './useTemplateManager'
export * from './useModelManager'
export * from './useHistoryManager'
export * from './useModelSelectRefs'
export * from './useVariableManager'
export * from './useAutoScroll'
export * from './useClipboard'
export * from './useFullscreen'
export * from './useContextEditor'
export * from './useNaiveTheme'
export * from './useResponsiveTestLayout'
export * from './useTestModeConfig'
export * from './useFunctionMode'
export * from './useImageWorkspace'
export * from './useResponsive'
export * from './usePerformanceMonitor'
export * from './useDebounceThrottle'
export * from './useVirtualScroll'
export * from './useLazyLoad'
export * from './useAccessibility'
export * from './useFocusManager'
export * from './useAccessibilityTesting'
export * from 'vue-i18n'
export * from './useAppInitializer'
export * from './useTooltipTheme'
export * from './usePromptPreview'
export * from "./useToast";
export * from "./useModals";
export * from "./usePromptOptimizer";
export * from "./usePromptTester";
export * from "./usePromptHistory";
export * from "./useTemplateManager";
export * from "./useModelManager";
export * from "./useHistoryManager";
export * from "./useModelSelectRefs";
export * from "./useVariableManager";
export * from "./useAutoScroll";
export * from "./useClipboard";
export * from "./useFullscreen";
export * from "./useContextEditor";
export * from "./useNaiveTheme";
export * from "./useResponsiveTestLayout";
export * from "./useTestModeConfig";
export * from "./useFunctionMode";
export * from "./useBasicSubMode";
export * from "./useProSubMode";
export * from "./useImageSubMode";
export * from "./useImageWorkspace";
export * from "./useResponsive";
export * from "./usePerformanceMonitor";
export * from "./useDebounceThrottle";
export * from "./useVirtualScroll";
export * from "./useLazyLoad";
export * from "./useAccessibility";
export * from "./useFocusManager";
export * from "./useAccessibilityTesting";
export * from "vue-i18n";
export * from "./useAppInitializer";
export * from "./useTooltipTheme";
export * from "./usePromptPreview";

View File

@@ -0,0 +1,95 @@
import { ref, readonly, type Ref } from 'vue'
import type { AppServices } from '../types/services'
import { usePreferences } from './usePreferenceManager'
import { UI_SETTINGS_KEYS, type BasicSubMode } from '@prompt-optimizer/core'
interface UseBasicSubModeApi {
basicSubMode: Ref<BasicSubMode>
setBasicSubMode: (mode: BasicSubMode) => Promise<void>
switchToSystem: () => Promise<void>
switchToUser: () => Promise<void>
ensureInitialized: () => Promise<void>
}
let singleton: {
mode: Ref<BasicSubMode>
initialized: boolean
initializing: Promise<void> | null
} | null = null
/**
* 基础模式的子模式单例
* - 默认值为 'system'(系统提示词优化)
* - 自动持久化
* - 独立于上下文模式和图像模式
*/
export function useBasicSubMode(services: Ref<AppServices | null>): UseBasicSubModeApi {
if (!singleton) {
singleton = {
mode: ref<BasicSubMode>('system'),
initialized: false,
initializing: null
}
}
const { getPreference, setPreference } = usePreferences(services)
const ensureInitialized = async () => {
if (singleton!.initialized) return
if (singleton!.initializing) {
await singleton!.initializing
return
}
singleton!.initializing = (async () => {
try {
const saved = await getPreference<BasicSubMode>(
UI_SETTINGS_KEYS.BASIC_SUB_MODE,
'system'
)
singleton!.mode.value = (saved === 'system' || saved === 'user')
? saved
: 'system'
console.log(`[useBasicSubMode] 初始化完成,当前值: ${singleton!.mode.value}`)
// 持久化默认值(如果未设置过)
if (saved !== 'system' && saved !== 'user') {
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
console.log('[useBasicSubMode] 首次初始化,已持久化默认值: system')
}
} catch (e) {
console.error('[useBasicSubMode] 初始化失败,使用默认值 system:', e)
// 读取失败则保持默认 'system',并尝试持久化
try {
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
} catch {
// 忽略设置失败错误
}
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
const setBasicSubMode = async (mode: BasicSubMode) => {
await ensureInitialized()
singleton!.mode.value = mode
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, mode)
console.log(`[useBasicSubMode] 子模式已切换并持久化: ${mode}`)
}
const switchToSystem = () => setBasicSubMode('system')
const switchToUser = () => setBasicSubMode('user')
return {
basicSubMode: readonly(singleton.mode) as Ref<BasicSubMode>,
setBasicSubMode,
switchToSystem,
switchToUser,
ensureInitialized
}
}

View File

@@ -0,0 +1,95 @@
import { ref, readonly, type Ref } from 'vue'
import type { AppServices } from '../types/services'
import { usePreferences } from './usePreferenceManager'
import { UI_SETTINGS_KEYS, type ImageSubMode } from '@prompt-optimizer/core'
interface UseImageSubModeApi {
imageSubMode: Ref<ImageSubMode>
setImageSubMode: (mode: ImageSubMode) => Promise<void>
switchToText2Image: () => Promise<void>
switchToImage2Image: () => Promise<void>
ensureInitialized: () => Promise<void>
}
let singleton: {
mode: Ref<ImageSubMode>
initialized: boolean
initializing: Promise<void> | null
} | null = null
/**
* 图像模式的子模式单例
* - 默认值为 'text2image'(文生图)
* - 自动持久化
* - 独立于基础模式和上下文模式
*/
export function useImageSubMode(services: Ref<AppServices | null>): UseImageSubModeApi {
if (!singleton) {
singleton = {
mode: ref<ImageSubMode>('text2image'),
initialized: false,
initializing: null
}
}
const { getPreference, setPreference } = usePreferences(services)
const ensureInitialized = async () => {
if (singleton!.initialized) return
if (singleton!.initializing) {
await singleton!.initializing
return
}
singleton!.initializing = (async () => {
try {
const saved = await getPreference<ImageSubMode>(
UI_SETTINGS_KEYS.IMAGE_SUB_MODE,
'text2image'
)
singleton!.mode.value = (saved === 'text2image' || saved === 'image2image')
? saved
: 'text2image'
console.log(`[useImageSubMode] 初始化完成,当前值: ${singleton!.mode.value}`)
// 持久化默认值(如果未设置过)
if (saved !== 'text2image' && saved !== 'image2image') {
await setPreference(UI_SETTINGS_KEYS.IMAGE_SUB_MODE, 'text2image')
console.log('[useImageSubMode] 首次初始化,已持久化默认值: text2image')
}
} catch (e) {
console.error('[useImageSubMode] 初始化失败,使用默认值 text2image:', e)
// 读取失败则保持默认 'text2image',并尝试持久化
try {
await setPreference(UI_SETTINGS_KEYS.IMAGE_SUB_MODE, 'text2image')
} catch {
// 忽略设置失败错误
}
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
const setImageSubMode = async (mode: ImageSubMode) => {
await ensureInitialized()
singleton!.mode.value = mode
await setPreference(UI_SETTINGS_KEYS.IMAGE_SUB_MODE, mode)
console.log(`[useImageSubMode] 子模式已切换并持久化: ${mode}`)
}
const switchToText2Image = () => setImageSubMode('text2image')
const switchToImage2Image = () => setImageSubMode('image2image')
return {
imageSubMode: readonly(singleton.mode) as Ref<ImageSubMode>,
setImageSubMode,
switchToText2Image,
switchToImage2Image,
ensureInitialized
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
import { ref, readonly, type Ref } from 'vue'
import type { AppServices } from '../types/services'
import { usePreferences } from './usePreferenceManager'
import { UI_SETTINGS_KEYS, type ProSubMode } from '@prompt-optimizer/core'
interface UseProSubModeApi {
proSubMode: Ref<ProSubMode>
setProSubMode: (mode: ProSubMode) => Promise<void>
switchToSystem: () => Promise<void>
switchToUser: () => Promise<void>
ensureInitialized: () => Promise<void>
}
let singleton: {
mode: Ref<ProSubMode>
initialized: boolean
initializing: Promise<void> | null
} | null = null
/**
* 上下文模式Pro模式的子模式单例。读取/写入 PreferenceService。
* - 默认值为 'system'(系统提示词优化)
* - 第一次调用时异步初始化
* - 状态独立于基础模式,实现不同功能模式下的子模式状态隔离
*/
export function useProSubMode(services: Ref<AppServices | null>): UseProSubModeApi {
if (!singleton) {
singleton = { mode: ref<ProSubMode>('system'), initialized: false, initializing: null }
}
const { getPreference, setPreference } = usePreferences(services)
const ensureInitialized = async () => {
if (singleton!.initialized) return
if (singleton!.initializing) {
await singleton!.initializing
return
}
singleton!.initializing = (async () => {
try {
// 读取 pro-sub-mode若不存在返回默认 'system'
const saved = await getPreference<ProSubMode>(UI_SETTINGS_KEYS.PRO_SUB_MODE, 'system')
singleton!.mode.value = (saved === 'system' || saved === 'user') ? saved : 'system'
console.log(`[useProSubMode] 初始化完成,当前值: ${singleton!.mode.value}`)
// 将默认值持久化(若未设置过)
if (saved !== 'system' && saved !== 'user') {
await setPreference(UI_SETTINGS_KEYS.PRO_SUB_MODE, 'system')
console.log('[useProSubMode] 首次初始化,已持久化默认值: system')
}
} catch (e) {
console.error('[useProSubMode] 初始化失败,使用默认值 system:', e)
// 读取失败则保持默认 'system',并尝试持久化
try {
await setPreference(UI_SETTINGS_KEYS.PRO_SUB_MODE, 'system')
} catch {
// 忽略设置失败错误
}
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
const setProSubMode = async (mode: ProSubMode) => {
await ensureInitialized()
singleton!.mode.value = mode
await setPreference(UI_SETTINGS_KEYS.PRO_SUB_MODE, mode)
console.log(`[useProSubMode] 子模式已切换并持久化: ${mode}`)
}
const switchToSystem = () => setProSubMode('system')
const switchToUser = () => setProSubMode('user')
return {
proSubMode: readonly(singleton.mode) as Ref<ProSubMode>,
setProSubMode,
switchToSystem,
switchToUser,
ensureInitialized
}
}

View File

@@ -133,6 +133,7 @@ export default {
globalVariables: "Global Variables",
contextVariables: "Context Variables",
tools: "Tool Management",
toolManager: "Tool Management",
},
preview: {
title: "Preview",
@@ -991,6 +992,7 @@ export default {
},
test: {
title: "Test",
areaTitle: "Test Area",
content: "Test Content",
placeholder: "Enter content to test...",
modes: {

View File

@@ -131,6 +131,7 @@ export default {
globalVariables: "全局变量",
contextVariables: "会话变量",
tools: "工具管理",
toolManager: "工具管理",
},
preview: {
title: "预览",
@@ -959,6 +960,7 @@ export default {
},
test: {
title: "测试",
areaTitle: "测试区域",
content: "测试内容",
placeholder: "请输入要测试的内容...",
modes: {

View File

@@ -186,6 +186,7 @@ export { quickTemplateManager } from "./data/quickTemplates";
// 导出图像模式组件与核心图像服务(转发 core 能力)
export { default as ImageWorkspace } from "./components/image-mode/ImageWorkspace.vue";
export { default as ImageModeSelector } from "./components/image-mode/ImageModeSelector.vue";
export {
ImageModelManager,
createImageModelManager,

View File

@@ -20,10 +20,34 @@
<!-- Core Navigation Slot -->
<template #core-nav>
<FunctionModeSelector
:modelValue="functionMode"
@update:modelValue="handleModeSelect"
/>
<NSpace :size="12" align="center">
<!-- 功能模式选择器 -->
<FunctionModeSelector
:modelValue="functionMode"
@update:modelValue="handleModeSelect"
/>
<!-- 子模式选择器 - 基础模式 -->
<OptimizationModeSelectorUI
v-if="functionMode === 'basic'"
:modelValue="basicSubMode"
@change="handleBasicSubModeChange"
/>
<!-- 子模式选择器 - 上下文模式 -->
<OptimizationModeSelectorUI
v-if="functionMode === 'pro'"
:modelValue="proSubMode"
@change="handleProSubModeChange"
/>
<!-- 子模式选择器 - 图像模式 -->
<ImageModeSelector
v-if="functionMode === 'image'"
:modelValue="imageSubMode"
@change="handleImageSubModeChange"
/>
</NSpace>
</template>
<!-- Actions Slot -->
@@ -185,14 +209,6 @@
@open-prompt-preview="handleOpenPromptPreview"
@open-test-preview="showPreviewPanel = true"
>
<!-- 优化模式选择器插槽 -->
<template #optimization-mode-selector>
<OptimizationModeSelectorUI
v-model="selectedOptimizationMode"
@change="handleOptimizationModeChange"
/>
</template>
<!-- 优化模型选择插槽 -->
<template #optimize-model-select>
<SelectWithConfig
@@ -365,14 +381,6 @@
@open-prompt-preview="handleOpenPromptPreview"
@open-test-preview="showPreviewPanel = true"
>
<!-- 优化模式选择器插槽 -->
<template #optimization-mode-selector>
<OptimizationModeSelectorUI
v-model="selectedOptimizationMode"
@change="handleOptimizationModeChange"
/>
</template>
<!-- 优化模型选择插槽 -->
<template #optimize-model-select>
<SelectWithConfig
@@ -531,16 +539,6 @@
"
@open-preview="handleOpenInputPreview"
>
<template #optimization-mode-selector>
<OptimizationModeSelectorUI
v-model="
selectedOptimizationMode
"
@change="
handleOptimizationModeChange
"
/>
</template>
<template #model-select>
<SelectWithConfig
v-model="
@@ -973,6 +971,7 @@ import {
NFlex,
NModal,
NScrollbar,
NSpace,
useMessage,
} from "naive-ui";
import hljs from "highlight.js/lib/core";
@@ -996,6 +995,7 @@ import {
UpdaterIcon,
VariableManagerModal,
ImageWorkspace,
ImageModeSelector,
FunctionModeSelector,
ConversationManager,
OutputDisplay,
@@ -1021,6 +1021,9 @@ import {
useResponsiveTestLayout,
useTestModeConfig,
useFunctionMode,
useBasicSubMode,
useProSubMode,
useImageSubMode,
usePromptPreview,
// i18n functions
@@ -1101,6 +1104,12 @@ const promptPanelRef = ref<{
// 高级模式状态
const { functionMode, setFunctionMode } = useFunctionMode(services as any);
// 三种功能模式的子模式持久化(独立存储)
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services as any);
const { proSubMode, setProSubMode } = useProSubMode(services as any);
const { imageSubMode, setImageSubMode } = useImageSubMode(services as any);
const advancedModeEnabled = computed({
get: () => functionMode.value === "pro",
set: (val: boolean) => {
@@ -1111,6 +1120,33 @@ const advancedModeEnabled = computed({
// 处理功能模式变化
const handleModeSelect = async (mode: "basic" | "pro" | "image") => {
await setFunctionMode(mode);
// 恢复各功能模式独立的子模式状态
if (mode === "basic") {
const { ensureInitialized } = useBasicSubMode(services as any);
await ensureInitialized();
selectedOptimizationMode.value = basicSubMode.value as OptimizationMode;
console.log(
`[App] 切换到基础模式,已恢复子模式: ${basicSubMode.value}`,
);
} else if (mode === "pro") {
const { ensureInitialized } = useProSubMode(services as any);
await ensureInitialized();
selectedOptimizationMode.value = proSubMode.value as OptimizationMode;
// 同步到 contextMode关键否则界面不会切换
await handleContextModeChange(
proSubMode.value as import("@prompt-optimizer/core").ContextMode,
);
console.log(
`[App] 切换到上下文模式,已恢复子模式: ${proSubMode.value}`,
);
} else if (mode === "image") {
const { ensureInitialized } = useImageSubMode(services as any);
await ensureInitialized();
console.log(
`[App] 切换到图像模式,已恢复子模式: ${imageSubMode.value}`,
);
}
};
// 测试内容状态 - 新增
@@ -1301,8 +1337,16 @@ const handleTestPanelVariableChange = async (name: string, value: string) => {
// 同步 contextManagement 中的 contextMode 到我们的 contextMode ref
watch(
contextManagement.contextMode,
(newMode) => {
async (newMode) => {
contextMode.value = newMode;
// Phase 1: 当 contextMode 变化时,如果在上下文模式下,持久化子模式
if (functionMode.value === "pro") {
await setProSubMode(
newMode as import("@prompt-optimizer/core").ProSubMode,
);
selectedOptimizationMode.value = newMode as OptimizationMode;
}
},
{ immediate: true },
);
@@ -1434,7 +1478,9 @@ const refreshTextModels = async () => {
textModelOptions.value =
DataTransformer.modelsToSelectOptions(enabledModels);
const availableKeys = new Set(textModelOptions.value.map((opt) => opt.value));
const availableKeys = new Set(
textModelOptions.value.map((opt) => opt.value),
);
const fallbackValue = textModelOptions.value[0]?.value || "";
const selectionReady = modelManager.isModelSelectionReady;
@@ -1517,6 +1563,30 @@ watch(services, async (newServices) => {
// 确保功能模式已初始化(默认 basic
// useFunctionMode 内部已处理默认值与持久化
// Phase 1: 初始化各功能模式的子模式持久化
// 根据当前功能模式,从存储恢复对应的子模式选择
if (functionMode.value === "basic") {
const { ensureInitialized } = useBasicSubMode(services as any);
await ensureInitialized();
// 同步到 selectedOptimizationMode 以保持兼容性
selectedOptimizationMode.value = basicSubMode.value as OptimizationMode;
console.log(`[App] 基础模式子模式已恢复: ${basicSubMode.value}`);
} else if (functionMode.value === "pro") {
const { ensureInitialized } = useProSubMode(services as any);
await ensureInitialized();
// 同步到 selectedOptimizationMode 以保持兼容性
selectedOptimizationMode.value = proSubMode.value as OptimizationMode;
// 同步到 contextMode关键否则界面不会切换
await handleContextModeChange(
proSubMode.value as import("@prompt-optimizer/core").ContextMode,
);
console.log(`[App] 上下文模式子模式已恢复: ${proSubMode.value}`);
} else if (functionMode.value === "image") {
const { ensureInitialized } = useImageSubMode(services as any);
await ensureInitialized();
console.log(`[App] 图像模式子模式已恢复: ${imageSubMode.value}`);
}
console.log("All services and composables initialized.");
// 监听全局历史刷新事件(来自图像模式)
@@ -1726,8 +1796,19 @@ const openTemplateManager = (
};
// 处理优化模式变更
const handleOptimizationModeChange = async (mode: OptimizationMode) => {
selectedOptimizationMode.value = mode;
// 基础模式子模式变更处理器
const handleBasicSubModeChange = async (mode: OptimizationMode) => {
await setBasicSubMode(
mode as import("@prompt-optimizer/core").BasicSubMode,
);
selectedOptimizationMode.value = mode; // 保持兼容性
console.log(`[App] 基础模式子模式已切换并持久化: ${mode}`);
};
// 上下文模式子模式变更处理器
const handleProSubModeChange = async (mode: OptimizationMode) => {
await setProSubMode(mode as import("@prompt-optimizer/core").ProSubMode);
selectedOptimizationMode.value = mode; // 保持兼容性
// 🔧 同步更新 contextMode确保两者一致避免重复调用
if (services.value?.contextMode.value !== mode) {
@@ -1735,6 +1816,36 @@ const handleOptimizationModeChange = async (mode: OptimizationMode) => {
mode as import("@prompt-optimizer/core").ContextMode,
);
}
console.log(`[App] 上下文模式子模式已切换并持久化: ${mode}`);
};
// 图像模式子模式变更处理器
const handleImageSubModeChange = async (
mode: import("@prompt-optimizer/core").ImageSubMode,
) => {
await setImageSubMode(mode);
console.log(`[App] 图像模式子模式已切换并持久化: ${mode}`);
// 通知 ImageWorkspace 更新
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("image-submode-changed", {
detail: { mode },
}),
);
}
};
// 🗑️ 废弃的统一处理器(保留兼容性)
const handleOptimizationModeChange = async (mode: OptimizationMode) => {
console.warn(
"[App] handleOptimizationModeChange 已废弃,请使用各模式独立的处理器",
);
if (functionMode.value === "basic") {
await handleBasicSubModeChange(mode);
} else if (functionMode.value === "pro") {
await handleProSubModeChange(mode);
}
};
// 处理模板语言变化
@@ -1886,6 +1997,23 @@ const handleHistoryReuse = async (context: {
// 如果目标模式与当前模式不同,自动切换
if (targetMode !== selectedOptimizationMode.value) {
selectedOptimizationMode.value = targetMode;
// 根据功能模式分别处理子模式的持久化
if (functionMode.value === "basic") {
// 基础模式:持久化子模式选择
await setBasicSubMode(
targetMode as import("@prompt-optimizer/core").BasicSubMode,
);
} else if (functionMode.value === "pro") {
// 上下文模式:持久化子模式并同步 contextMode
await setProSubMode(
targetMode as import("@prompt-optimizer/core").ProSubMode,
);
await handleContextModeChange(
targetMode as import("@prompt-optimizer/core").ContextMode,
);
}
useToast().info(
t("toast.info.optimizationModeAutoSwitched", {
mode:
@@ -2180,6 +2308,23 @@ const handleUseFavorite = async (favorite: any) => {
favOptimizationMode !== selectedOptimizationMode.value
) {
selectedOptimizationMode.value = favOptimizationMode;
// 根据功能模式分别处理子模式的持久化
if (functionMode.value === "basic") {
// 基础模式:持久化子模式选择
await setBasicSubMode(
favOptimizationMode as import("@prompt-optimizer/core").BasicSubMode,
);
} else if (functionMode.value === "pro") {
// 上下文模式:持久化子模式并同步 contextMode
await setProSubMode(
favOptimizationMode as import("@prompt-optimizer/core").ProSubMode,
);
await handleContextModeChange(
favOptimizationMode as import("@prompt-optimizer/core").ContextMode,
);
}
useToast().info(
t("toast.info.optimizationModeAutoSwitched", {
mode:
@@ -2198,6 +2343,24 @@ const handleUseFavorite = async (favorite: any) => {
useToast().info(
`已自动切换到${targetFunctionMode === "pro" ? "上下文" : "基础"}模式`,
);
// 功能模式切换后,如果有优化模式信息,确保同步各自的子模式持久化
if (favOptimizationMode) {
if (targetFunctionMode === "basic") {
// 基础模式:持久化子模式选择
await setBasicSubMode(
favOptimizationMode as import("@prompt-optimizer/core").BasicSubMode,
);
} else if (targetFunctionMode === "pro") {
// 上下文模式:持久化子模式并同步 contextMode
await setProSubMode(
favOptimizationMode as import("@prompt-optimizer/core").ProSubMode,
);
await handleContextModeChange(
favOptimizationMode as import("@prompt-optimizer/core").ContextMode,
);
}
}
}
// 4. 将收藏的提示词内容设置到输入框