mirror of
https://github.com/linshenkx/prompt-optimizer.git
synced 2026-05-06 21:50:27 +08:00
feat(ui): 实现三大功能模式的独立子模式持久化功能
- 新增三个子模式管理Composable (useBasicSubMode/useProSubMode/useImageSubMode) - 实现基础/上下文/图像模式的完全独立状态存储 - 添加UI_SETTINGS_KEYS常量用于子模式存储键管理 - 更新App.vue初始化逻辑支持三模式独立恢复 - 修复图像模式刷新后文件上传按钮不显示的bug - 完善历史记录和收藏恢复时的子模式持久化 - 新增国际化文本支持子模式切换提示 - 归档完整开发文档到126-submode-persistence 核心特性: - 状态隔离: 三个功能模式维护完全独立的子模式状态 - 跨页面同步: 使用自定义事件实现组件间状态同步 - 双层状态一致性: 导航层和组件层状态保持同步 - 异步初始化: 非阻塞式状态恢复机制
This commit is contained in:
140
docs/archives/126-submode-persistence/README.md
Normal file
140
docs/archives/126-submode-persistence/README.md
Normal 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 & 用户
|
||||
1290
docs/archives/126-submode-persistence/design.md
Normal file
1290
docs/archives/126-submode-persistence/design.md
Normal file
File diff suppressed because it is too large
Load Diff
452
docs/archives/126-submode-persistence/experience.md
Normal file
452
docs/archives/126-submode-persistence/experience.md
Normal 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 & 用户
|
||||
529
docs/archives/126-submode-persistence/implementation.md
Normal file
529
docs/archives/126-submode-persistence/implementation.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
## 📖 使用建议
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 中聚合导出。
|
||||
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
// 核心状态
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
95
packages/ui/src/composables/useBasicSubMode.ts
Normal file
95
packages/ui/src/composables/useBasicSubMode.ts
Normal 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
|
||||
}
|
||||
}
|
||||
95
packages/ui/src/composables/useImageSubMode.ts
Normal file
95
packages/ui/src/composables/useImageSubMode.ts
Normal 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
84
packages/ui/src/composables/useProSubMode.ts
Normal file
84
packages/ui/src/composables/useProSubMode.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -131,6 +131,7 @@ export default {
|
||||
globalVariables: "全局变量",
|
||||
contextVariables: "会话变量",
|
||||
tools: "工具管理",
|
||||
toolManager: "工具管理",
|
||||
},
|
||||
preview: {
|
||||
title: "预览",
|
||||
@@ -959,6 +960,7 @@ export default {
|
||||
},
|
||||
test: {
|
||||
title: "测试",
|
||||
areaTitle: "测试区域",
|
||||
content: "测试内容",
|
||||
placeholder: "请输入要测试的内容...",
|
||||
modes: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. 将收藏的提示词内容设置到输入框
|
||||
|
||||
Reference in New Issue
Block a user