10 KiB
10 KiB
新聊天 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 顶部添加:
#import "KBChatTableView.h"
步骤 2:修改属性声明
将:
@property (nonatomic, strong) KBAiChatView *chatView;
改为:
@property (nonatomic, strong) KBChatTableView *chatView;
步骤 3:修改 setupUI 方法
将:
self.chatView = [[KBAiChatView alloc] init];
改为:
self.chatView = [[KBChatTableView alloc] init];
步骤 4:修改消息添加逻辑
添加用户消息(保持不变)
[self.chatView addUserMessage:finalText];
添加 AI 消息(需要修改)
原来的代码:
[self.chatView addAssistantMessage:polishedText];
[self.chatView markLastAssistantMessageComplete];
新的代码:
// 计算音频时长(如果有音频数据)
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:修改打字机效果(如果使用)
原来的代码:
[self.chatView addAssistantMessage:@""];
[self.chatView updateLastAssistantMessage:token];
[self.chatView markLastAssistantMessageComplete];
新的代码:
// 1. 添加空消息占位
[self.chatView addAssistantMessage:@""
audioDuration:0
audioData:nil];
// 2. 逐步更新
[self.chatView updateLastAssistantMessage:token];
// 3. 完成后标记并添加音频
[self.chatView markLastAssistantMessageComplete];
// 如果有音频,需要更新最后一条消息的音频数据
// 注意:当前实现不支持后续添加音频,建议在完成时重新添加消息
📝 完整示例:修改 deepgramStreamingManagerDidReceiveFinalTranscript
- (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(使用测试页面)
在任意地方跳转到测试页面:
KBChatTestVC *testVC = [[KBChatTestVC alloc] init];
[self.navigationController pushViewController:testVC animated:YES];
2. 集成到 KBAiMainVC
按照上面的步骤修改 KBAiMainVC.m
3. 验证功能
- ✅ 用户消息显示在右侧
- ✅ AI 消息显示在左侧
- ✅ 时间戳自动插入
- ✅ 语音按钮可点击播放
- ✅ 语音时长正确显示
- ✅ 消息自动滚动到底部
🎨 自定义样式
修改气泡颜色
用户消息(KBChatUserMessageCell.m):
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.94 green:0.94 blue:0.94 alpha:1.0];
self.messageLabel.textColor = [UIColor blackColor];
AI 消息(KBChatAssistantMessageCell.m):
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7];
self.messageLabel.textColor = [UIColor whiteColor];
修改时间戳间隔
在 KBChatTableView.m 中:
static const NSTimeInterval kTimestampInterval = 5 * 60; // 改为你想要的秒数
修改气泡圆角
在对应的 Cell 中:
self.bubbleView.layer.cornerRadius = 16; // 改为你想要的值
⚠️ 注意事项
-
音频格式:确保
audioData是 AVAudioPlayer 支持的格式(MP3、AAC、M4A 等) -
音频会话:播放音频前确保配置了 AVAudioSession(已在
AudioSessionManager中处理) -
内存管理:如果消息量很大,考虑:
- 限制消息数量(如只保留最近 100 条)
- 清除旧消息的音频数据
- 实现分页加载
-
线程安全:所有 UI 更新必须在主线程执行
-
音频时长计算:
- 方法 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 界面!