364 lines
10 KiB
Markdown
364 lines
10 KiB
Markdown
|
|
# 新聊天 UI 集成指南
|
|||
|
|
|
|||
|
|
## 📦 已创建的文件
|
|||
|
|
|
|||
|
|
### Model(数据模型)
|
|||
|
|
- `KBChatMessage.h/m` - 消息模型(支持用户/AI/时间戳三种类型)
|
|||
|
|
|
|||
|
|
### View(视图组件)
|
|||
|
|
- `KBChatUserMessageCell.h/m` - 用户消息 Cell(右侧气泡)
|
|||
|
|
- `KBChatAssistantMessageCell.h/m` - AI 消息 Cell(左侧气泡 + 语音按钮)
|
|||
|
|
- `KBChatTimeCell.h/m` - 时间戳 Cell(居中显示)
|
|||
|
|
- `KBChatTableView.h/m` - 聊天列表视图(主容器)
|
|||
|
|
|
|||
|
|
### ViewController(测试页面)
|
|||
|
|
- `KBChatTestVC.h/m` - 测试页面(可选,用于演示)
|
|||
|
|
|
|||
|
|
### 文档
|
|||
|
|
- `KBChatTableView_Usage.md` - 使用说明
|
|||
|
|
- `集成指南.md` - 本文档
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 核心特性
|
|||
|
|
|
|||
|
|
✅ **三种消息类型**
|
|||
|
|
- 用户消息:右侧浅色气泡
|
|||
|
|
- AI 消息:左侧深色气泡 + 语音播放按钮
|
|||
|
|
- 时间戳:居中显示,自动插入(5 分钟间隔)
|
|||
|
|
|
|||
|
|
✅ **语音播放**
|
|||
|
|
- 点击播放/暂停
|
|||
|
|
- 显示语音时长(如 "6"")
|
|||
|
|
- 播放状态图标切换
|
|||
|
|
|
|||
|
|
✅ **打字机效果**
|
|||
|
|
- 支持实时更新 AI 消息文本
|
|||
|
|
- 流式显示
|
|||
|
|
|
|||
|
|
✅ **自动滚动**
|
|||
|
|
- 新消息自动滚动到底部
|
|||
|
|
- 平滑动画
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 集成到 KBAiMainVC
|
|||
|
|
|
|||
|
|
### 步骤 1:修改 import
|
|||
|
|
|
|||
|
|
在 `KBAiMainVC.m` 顶部添加:
|
|||
|
|
|
|||
|
|
```objective-c
|
|||
|
|
#import "KBChatTableView.h"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 2:修改属性声明
|
|||
|
|
|
|||
|
|
将:
|
|||
|
|
```objective-c
|
|||
|
|
@property (nonatomic, strong) KBAiChatView *chatView;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
改为:
|
|||
|
|
```objective-c
|
|||
|
|
@property (nonatomic, strong) KBChatTableView *chatView;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 3:修改 setupUI 方法
|
|||
|
|
|
|||
|
|
将:
|
|||
|
|
```objective-c
|
|||
|
|
self.chatView = [[KBAiChatView alloc] init];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
改为:
|
|||
|
|
```objective-c
|
|||
|
|
self.chatView = [[KBChatTableView alloc] init];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 4:修改消息添加逻辑
|
|||
|
|
|
|||
|
|
#### 添加用户消息(保持不变)
|
|||
|
|
```objective-c
|
|||
|
|
[self.chatView addUserMessage:finalText];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 添加 AI 消息(需要修改)
|
|||
|
|
|
|||
|
|
**原来的代码:**
|
|||
|
|
```objective-c
|
|||
|
|
[self.chatView addAssistantMessage:polishedText];
|
|||
|
|
[self.chatView markLastAssistantMessageComplete];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**新的代码:**
|
|||
|
|
```objective-c
|
|||
|
|
// 计算音频时长(如果有音频数据)
|
|||
|
|
NSTimeInterval duration = 0;
|
|||
|
|
if (audioData && audioData.length > 0) {
|
|||
|
|
// 方法 1:从 AVAudioPlayer 获取准确时长
|
|||
|
|
NSError *error = nil;
|
|||
|
|
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:audioData error:&error];
|
|||
|
|
if (!error && player) {
|
|||
|
|
duration = player.duration;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 方法 2:估算时长(如果知道采样率)
|
|||
|
|
// duration = audioData.length / (sampleRate * channels * bytesPerSample);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[self.chatView addAssistantMessage:polishedText
|
|||
|
|
audioDuration:duration
|
|||
|
|
audioData:audioData];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 5:修改打字机效果(如果使用)
|
|||
|
|
|
|||
|
|
**原来的代码:**
|
|||
|
|
```objective-c
|
|||
|
|
[self.chatView addAssistantMessage:@""];
|
|||
|
|
[self.chatView updateLastAssistantMessage:token];
|
|||
|
|
[self.chatView markLastAssistantMessageComplete];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**新的代码:**
|
|||
|
|
```objective-c
|
|||
|
|
// 1. 添加空消息占位
|
|||
|
|
[self.chatView addAssistantMessage:@""
|
|||
|
|
audioDuration:0
|
|||
|
|
audioData:nil];
|
|||
|
|
|
|||
|
|
// 2. 逐步更新
|
|||
|
|
[self.chatView updateLastAssistantMessage:token];
|
|||
|
|
|
|||
|
|
// 3. 完成后标记并添加音频
|
|||
|
|
[self.chatView markLastAssistantMessageComplete];
|
|||
|
|
|
|||
|
|
// 如果有音频,需要更新最后一条消息的音频数据
|
|||
|
|
// 注意:当前实现不支持后续添加音频,建议在完成时重新添加消息
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 完整示例:修改 deepgramStreamingManagerDidReceiveFinalTranscript
|
|||
|
|
|
|||
|
|
```objective-c
|
|||
|
|
- (void)deepgramStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
|
|||
|
|
if (text.length > 0) {
|
|||
|
|
if (self.deepgramFullText.length > 0) {
|
|||
|
|
[self.deepgramFullText appendString:@" "];
|
|||
|
|
}
|
|||
|
|
[self.deepgramFullText appendString:text];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.transcriptLabel.text = self.deepgramFullText;
|
|||
|
|
self.statusLabel.text = @"识别完成";
|
|||
|
|
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|||
|
|
|
|||
|
|
NSString *finalText = [self.deepgramFullText copy];
|
|||
|
|
if (finalText.length == 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加用户消息
|
|||
|
|
[self.chatView addUserMessage:finalText];
|
|||
|
|
|
|||
|
|
if (self.elevenLabsApiKey.length == 0 || self.elevenLabsVoiceId.length == 0) {
|
|||
|
|
[KBHUD showError:@"请先配置 ElevenLabs API Key/VoiceId"];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
__weak typeof(self) weakSelf = self;
|
|||
|
|
[KBHUD showWithStatus:@"润色中..."];
|
|||
|
|
|
|||
|
|
[self.aiVM requestChatMessageWithContent:finalText
|
|||
|
|
completion:^(KBAiMessageResponse *_Nullable response,
|
|||
|
|
NSError *_Nullable error) {
|
|||
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|||
|
|
if (!strongSelf) return;
|
|||
|
|
|
|||
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|||
|
|
if (error) {
|
|||
|
|
[KBHUD dismiss];
|
|||
|
|
[KBHUD showError:error.localizedDescription ?: @"润色失败"];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NSString *polishedText = response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
|||
|
|
|
|||
|
|
if (polishedText.length == 0) {
|
|||
|
|
[KBHUD dismiss];
|
|||
|
|
[KBHUD showError:@"润色结果为空"];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[KBHUD showWithStatus:@"生成语音..."];
|
|||
|
|
|
|||
|
|
[strongSelf.aiVM requestElevenLabsSpeechWithText:polishedText
|
|||
|
|
voiceId:strongSelf.elevenLabsVoiceId
|
|||
|
|
apiKey:strongSelf.elevenLabsApiKey
|
|||
|
|
outputFormat:nil
|
|||
|
|
modelId:nil
|
|||
|
|
completion:^(NSData *_Nullable audioData,
|
|||
|
|
NSError *_Nullable ttsError) {
|
|||
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|||
|
|
[KBHUD dismiss];
|
|||
|
|
|
|||
|
|
if (ttsError) {
|
|||
|
|
[KBHUD showError:ttsError.localizedDescription ?: @"语音生成失败"];
|
|||
|
|
// 即使语音失败,也添加文本消息
|
|||
|
|
[strongSelf.chatView addAssistantMessage:polishedText
|
|||
|
|
audioDuration:0
|
|||
|
|
audioData:nil];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算音频时长
|
|||
|
|
NSTimeInterval duration = 0;
|
|||
|
|
if (audioData && audioData.length > 0) {
|
|||
|
|
NSError *playerError = nil;
|
|||
|
|
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:audioData
|
|||
|
|
error:&playerError];
|
|||
|
|
if (!playerError && player) {
|
|||
|
|
duration = player.duration;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加 AI 消息(带语音)
|
|||
|
|
[strongSelf.chatView addAssistantMessage:polishedText
|
|||
|
|
audioDuration:duration
|
|||
|
|
audioData:audioData];
|
|||
|
|
});
|
|||
|
|
}];
|
|||
|
|
});
|
|||
|
|
}];
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 测试步骤
|
|||
|
|
|
|||
|
|
### 1. 测试新 UI(使用测试页面)
|
|||
|
|
|
|||
|
|
在任意地方跳转到测试页面:
|
|||
|
|
|
|||
|
|
```objective-c
|
|||
|
|
KBChatTestVC *testVC = [[KBChatTestVC alloc] init];
|
|||
|
|
[self.navigationController pushViewController:testVC animated:YES];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 集成到 KBAiMainVC
|
|||
|
|
|
|||
|
|
按照上面的步骤修改 `KBAiMainVC.m`
|
|||
|
|
|
|||
|
|
### 3. 验证功能
|
|||
|
|
|
|||
|
|
- ✅ 用户消息显示在右侧
|
|||
|
|
- ✅ AI 消息显示在左侧
|
|||
|
|
- ✅ 时间戳自动插入
|
|||
|
|
- ✅ 语音按钮可点击播放
|
|||
|
|
- ✅ 语音时长正确显示
|
|||
|
|
- ✅ 消息自动滚动到底部
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 自定义样式
|
|||
|
|
|
|||
|
|
### 修改气泡颜色
|
|||
|
|
|
|||
|
|
**用户消息**(`KBChatUserMessageCell.m`):
|
|||
|
|
```objective-c
|
|||
|
|
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.94 green:0.94 blue:0.94 alpha:1.0];
|
|||
|
|
self.messageLabel.textColor = [UIColor blackColor];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**AI 消息**(`KBChatAssistantMessageCell.m`):
|
|||
|
|
```objective-c
|
|||
|
|
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7];
|
|||
|
|
self.messageLabel.textColor = [UIColor whiteColor];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修改时间戳间隔
|
|||
|
|
|
|||
|
|
在 `KBChatTableView.m` 中:
|
|||
|
|
```objective-c
|
|||
|
|
static const NSTimeInterval kTimestampInterval = 5 * 60; // 改为你想要的秒数
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修改气泡圆角
|
|||
|
|
|
|||
|
|
在对应的 Cell 中:
|
|||
|
|
```objective-c
|
|||
|
|
self.bubbleView.layer.cornerRadius = 16; // 改为你想要的值
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ 注意事项
|
|||
|
|
|
|||
|
|
1. **音频格式**:确保 `audioData` 是 AVAudioPlayer 支持的格式(MP3、AAC、M4A 等)
|
|||
|
|
|
|||
|
|
2. **音频会话**:播放音频前确保配置了 AVAudioSession(已在 `AudioSessionManager` 中处理)
|
|||
|
|
|
|||
|
|
3. **内存管理**:如果消息量很大,考虑:
|
|||
|
|
- 限制消息数量(如只保留最近 100 条)
|
|||
|
|
- 清除旧消息的音频数据
|
|||
|
|
- 实现分页加载
|
|||
|
|
|
|||
|
|
4. **线程安全**:所有 UI 更新必须在主线程执行
|
|||
|
|
|
|||
|
|
5. **音频时长计算**:
|
|||
|
|
- 方法 1:使用 AVAudioPlayer 获取准确时长(推荐)
|
|||
|
|
- 方法 2:根据采样率估算(不够准确)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🐛 常见问题
|
|||
|
|
|
|||
|
|
### Q1: 语音按钮不显示?
|
|||
|
|
**A:** 检查 `audioData` 是否为 nil 或长度为 0
|
|||
|
|
|
|||
|
|
### Q2: 点击语音按钮没反应?
|
|||
|
|
**A:** 检查音频数据格式是否正确,查看控制台日志
|
|||
|
|
|
|||
|
|
### Q3: 时间戳不显示?
|
|||
|
|
**A:** 检查消息的 `timestamp` 是否正确设置
|
|||
|
|
|
|||
|
|
### Q4: 消息不自动滚动?
|
|||
|
|
**A:** 确保在主线程调用 `scrollToBottom`
|
|||
|
|
|
|||
|
|
### Q5: 气泡宽度不对?
|
|||
|
|
**A:** 检查 Masonry 约束,确保 `multipliedBy(0.75)` 生效
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📚 相关文件
|
|||
|
|
|
|||
|
|
- 使用说明:`KBChatTableView_Usage.md`
|
|||
|
|
- 测试页面:`KBChatTestVC.h/m`
|
|||
|
|
- 原有实现:`KBAiChatView.h/m`(可保留作为备份)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 完成清单
|
|||
|
|
|
|||
|
|
- [ ] 创建所有新文件
|
|||
|
|
- [ ] 修改 `KBAiMainVC.m` 的 import
|
|||
|
|
- [ ] 修改属性声明
|
|||
|
|
- [ ] 修改 setupUI 方法
|
|||
|
|
- [ ] 修改消息添加逻辑
|
|||
|
|
- [ ] 测试用户消息显示
|
|||
|
|
- [ ] 测试 AI 消息显示
|
|||
|
|
- [ ] 测试语音播放功能
|
|||
|
|
- [ ] 测试时间戳显示
|
|||
|
|
- [ ] 测试打字机效果
|
|||
|
|
- [ ] 自定义样式(可选)
|
|||
|
|
- [ ] 删除旧的 `KBAiChatView`(可选)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎉 完成!
|
|||
|
|
|
|||
|
|
按照以上步骤完成集成后,你将拥有一个功能完整、美观的聊天 UI 界面!
|