219 lines
6.0 KiB
Markdown
219 lines
6.0 KiB
Markdown
# Swipe & Reroll 机制对比分析
|
||
|
||
## SillyTavern 官方机制
|
||
|
||
根据 SillyTavern 的设计,swipe 和 reroll 的核心逻辑如下:
|
||
|
||
### 1. **Swipe(版本切换)**
|
||
|
||
**数据结构:**
|
||
```json
|
||
{
|
||
"floor": 2,
|
||
"mes": "当前显示的内容",
|
||
"swipes": [
|
||
"第一个版本的回复",
|
||
"第二个版本的回复",
|
||
"第三个版本的回复"
|
||
],
|
||
"swipe_id": 1 // 当前显示的版本索引(从0开始)
|
||
}
|
||
```
|
||
|
||
**行为规则:**
|
||
- ✅ `mes` 字段始终显示当前选中的版本内容
|
||
- ✅ `swipes` 数组存储所有历史版本
|
||
- ✅ `swipe_id` 指向当前在 `swipes` 数组中的索引
|
||
- ✅ 切换 swipe 时只更新 `swipe_id` 和 `mes`,不创建新楼层
|
||
|
||
### 2. **Reroll(重新生成)**
|
||
|
||
**核心原则:**
|
||
- ❌ **不创建新的消息楼层**
|
||
- ✅ **在当前 AI 消息的 swipes 数组中添加新版本**
|
||
- ✅ **使用上一条用户消息作为上下文重新生成**
|
||
- ✅ **生成完成后自动切换到新版本**
|
||
|
||
**流程:**
|
||
```
|
||
1. 用户触发 reroll(右键菜单或键盘快捷键)
|
||
2. 系统找到该 AI 消息的上一条用户消息
|
||
3. 发送相同的用户输入到后端
|
||
4. 后端生成新的回复
|
||
5. 前端将新回复添加到 swipes 数组末尾
|
||
6. 自动更新 swipe_id 指向新版本
|
||
7. 更新 mes 字段显示新内容
|
||
```
|
||
|
||
**示例:**
|
||
```javascript
|
||
// 初始状态
|
||
{
|
||
floor: 2,
|
||
mes: "版本A的内容",
|
||
swipes: ["版本A的内容"],
|
||
swipe_id: 0
|
||
}
|
||
|
||
// 用户点击 reroll 后
|
||
{
|
||
floor: 2, // ← 楼层不变
|
||
mes: "版本B的内容", // ← 显示新内容
|
||
swipes: [
|
||
"版本A的内容",
|
||
"版本B的内容" // ← 添加新版本
|
||
],
|
||
swipe_id: 1 // ← 自动切换到新版本
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 我们的实现
|
||
|
||
### ✅ 已正确实现的部分
|
||
|
||
#### 1. **Swipe 切换**
|
||
```javascript
|
||
// ChatBox.jsx - handleSwipeChange
|
||
const handleSwipeChange = (messageId, direction) => {
|
||
const message = messages.find(m => m.floor === messageId);
|
||
if (message && message.swipes && message.swipes.length > 0) {
|
||
const currentIndex = currentSwipeId[messageId] !== undefined
|
||
? currentSwipeId[messageId]
|
||
: message.swipe_id;
|
||
const newIndex = currentIndex + direction;
|
||
|
||
if (newIndex >= 0 && newIndex < message.swipes.length) {
|
||
setCurrentSwipeId({ [messageId]: newIndex });
|
||
}
|
||
}
|
||
};
|
||
```
|
||
✅ **符合 SillyTavern**:只更新索引,不修改数据结构
|
||
|
||
#### 2. **Swipe 控制按钮**
|
||
```jsx
|
||
{hasSwipes && !isUser && (
|
||
<div className="swipe-controls">
|
||
<button onClick={() => handleSwipeChange(message.floor, -1)}>◀</button>
|
||
<span>{currentSwipeIndex + 1}/{message.swipes.length}</span>
|
||
<button onClick={() => handleSwipeChange(message.floor, 1)}>▶</button>
|
||
</div>
|
||
)}
|
||
```
|
||
✅ **符合 SillyTavern**:所有有 swipe 的消息都显示控制按钮
|
||
|
||
#### 3. **键盘快捷键**
|
||
```javascript
|
||
// 左键:切换到上一个版本
|
||
if (e.key === 'ArrowLeft' && currentIndex > 0) {
|
||
handleSwipeChange(lastAiMessage.floor, -1);
|
||
}
|
||
|
||
// 右键:如果在最后一个版本,触发重roll;否则切换到下一个版本
|
||
if (e.key === 'ArrowRight') {
|
||
if (currentIndex >= lastAiMessage.swipes.length - 1) {
|
||
handleRerollMessage(lastAiMessage); // 触发重roll
|
||
} else {
|
||
handleSwipeChange(lastAiMessage.floor, 1);
|
||
}
|
||
}
|
||
```
|
||
✅ **符合 SillyTavern**:智能判断是否触发重roll
|
||
|
||
#### 4. **右键菜单重roll**
|
||
```jsx
|
||
{!contextMenu.message.is_user && (
|
||
<div className="context-menu-item" onClick={() => handleRerollMessage(contextMenu.message)}>
|
||
<span className="menu-icon">🔄</span>
|
||
<span className="menu-label">重roll</span>
|
||
</div>
|
||
)}
|
||
```
|
||
✅ **符合 SillyTavern**:只有 AI 消息显示重roll选项
|
||
|
||
### ✅ 重roll核心逻辑(刚刚修复)
|
||
|
||
#### ChatBoxSlice.jsx - sendMessage
|
||
```javascript
|
||
sendMessage: async (content, targetFloor = null) => {
|
||
const isReroll = targetFloor !== null;
|
||
|
||
if (isReroll) {
|
||
// 重roll模式
|
||
assistantFloor = targetFloor;
|
||
nextFloor = targetFloor; // ✅ 修复:设置 nextFloor
|
||
// 不添加新用户消息
|
||
} else {
|
||
// 正常模式
|
||
nextFloor = get().getNextFloor(messages);
|
||
userFloor = nextFloor;
|
||
assistantFloor = nextFloor + 1;
|
||
// 添加新用户消息和AI消息
|
||
}
|
||
}
|
||
```
|
||
|
||
#### WebSocket Complete 事件处理
|
||
```javascript
|
||
if (data.type === 'complete' && isReroll) {
|
||
// 获取现有的 swipes 数组
|
||
const existingSwipes = msg.swipes || [];
|
||
const currentMes = msg.mes;
|
||
|
||
// 如果当前内容不在 swipes 中,先添加它
|
||
let updatedSwipes = [...existingSwipes];
|
||
if (!updatedSwipes.includes(currentMes)) {
|
||
updatedSwipes.push(currentMes);
|
||
}
|
||
|
||
// 添加新生成的内容
|
||
updatedSwipes.push(assistantMessage);
|
||
|
||
// 更新消息
|
||
return {
|
||
...msg,
|
||
mes: assistantMessage, // 显示新内容
|
||
swipes: updatedSwipes, // 更新数组
|
||
swipe_id: updatedSwipes.length - 1 // 自动切换到新版本
|
||
};
|
||
}
|
||
```
|
||
✅ **完全符合 SillyTavern**:
|
||
- ❌ 不创建新楼层
|
||
- ✅ 在 swipes 数组中添加新版本
|
||
- ✅ 自动切换到新生成的版本
|
||
- ✅ 保留历史版本
|
||
|
||
---
|
||
|
||
## 对比总结
|
||
|
||
| 功能 | SillyTavern | 我们的实现 | 状态 |
|
||
|------|-------------|-----------|------|
|
||
| Swipe 数据结构 | `swipes` 数组 + `swipe_id` | ✅ 相同 | ✅ |
|
||
| Swipe 切换 | 只更新索引 | ✅ 相同 | ✅ |
|
||
| Swipe 按钮显示 | 所有有 swipe 的消息 | ✅ 相同 | ✅ |
|
||
| 键盘左右键切换 | 支持 | ✅ 相同 | ✅ |
|
||
| Reroll 不创建新楼层 | ✅ | ✅ | ✅ |
|
||
| Reroll 添加到 swipes | ✅ | ✅ | ✅ |
|
||
| Reroll 自动切换 | ✅ | ✅ | ✅ |
|
||
| Reroll 使用上文 | ✅ | ✅ | ✅ |
|
||
| 右键菜单重roll | ✅ | ✅ | ✅ |
|
||
| 最后一个swipe按右键重roll | ✅ | ✅ | ✅ |
|
||
|
||
## 结论
|
||
|
||
✅ **我们的实现与 SillyTavern 官方机制完全一致!**
|
||
|
||
所有核心功能都已正确实现:
|
||
1. ✅ Swipe 版本切换
|
||
2. ✅ Reroll 不创建新楼层
|
||
3. ✅ Reroll 添加到 swipes 数组
|
||
4. ✅ 自动切换到新版本
|
||
5. ✅ 键盘快捷键支持
|
||
6. ✅ 右键菜单支持
|
||
|
||
唯一的区别是 UI 样式和交互细节,但核心逻辑完全符合 SillyTavern 的设计规范。
|