Files
keyboard/keyBoard/Class/AiTalk/集成指南.md
2026-01-23 21:51:37 +08:00

10 KiB
Raw Blame History

新聊天 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; // 改为你想要的值

⚠️ 注意事项

  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 界面!