From e8b4b2c58a6cbf7073b81436592e006708e67631 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Mon, 26 Jan 2026 20:36:51 +0800 Subject: [PATCH] 2 --- keyBoard/Class/AiTalk/M/KBChatHistoryModel.h | 6 +- keyBoard/Class/AiTalk/M/聊天记录Model说明.md | 25 +- keyBoard/Class/AiTalk/V/KBChatTableView.h | 29 ++- keyBoard/Class/AiTalk/V/KBChatTableView.m | 251 +++++++++++-------- keyBoard/Class/AiTalk/V/KBPersonaChatCell.h | 7 + keyBoard/Class/AiTalk/V/KBPersonaChatCell.m | 132 +++++----- keyBoard/Class/AiTalk/VC/KBAIHomeVC.m | 58 ++++- 7 files changed, 338 insertions(+), 170 deletions(-) diff --git a/keyBoard/Class/AiTalk/M/KBChatHistoryModel.h b/keyBoard/Class/AiTalk/M/KBChatHistoryModel.h index 84c3aaa..feb715b 100644 --- a/keyBoard/Class/AiTalk/M/KBChatHistoryModel.h +++ b/keyBoard/Class/AiTalk/M/KBChatHistoryModel.h @@ -9,10 +9,10 @@ NS_ASSUME_NONNULL_BEGIN -/// 消息发送者类型 +/// 消息发送者类型(与后端保持一致) typedef NS_ENUM(NSInteger, KBChatSender) { - KBChatSenderUser = 0, // 用户 - KBChatSenderAssistant = 1 // AI 助手 + KBChatSenderUser = 1, // 用户(右侧显示) + KBChatSenderAssistant = 2 // AI 助手(左侧显示) }; /// 聊天记录模型 diff --git a/keyBoard/Class/AiTalk/M/聊天记录Model说明.md b/keyBoard/Class/AiTalk/M/聊天记录Model说明.md index 7f42830..30c325a 100644 --- a/keyBoard/Class/AiTalk/M/聊天记录Model说明.md +++ b/keyBoard/Class/AiTalk/M/聊天记录Model说明.md @@ -86,13 +86,13 @@ POST /chat/history "records": [ { "id": 1, - "sender": 0, // 0-用户,1-AI + "sender": 1, // 1-用户(右侧),2-AI(左侧) "content": "你好", "createdAt": "2026-01-26 10:00:00" }, { "id": 2, - "sender": 1, + "sender": 2, "content": "你好!有什么可以帮助你的吗?", "createdAt": "2026-01-26 10:00:05" } @@ -104,6 +104,10 @@ POST /chat/history } ``` +### sender 字段说明 +- **sender = 1**:用户消息(显示在右侧) +- **sender = 2**:AI 消息(显示在左侧) + --- ## 📝 使用示例 @@ -359,11 +363,24 @@ AiVM *aiVM = [[AiVM alloc] init]; ```objc typedef NS_ENUM(NSInteger, KBChatSender) { - KBChatSenderUser = 0, // 用户 - KBChatSenderAssistant = 1 // AI 助手 + KBChatSenderUser = 1, // 用户(右侧显示) + KBChatSenderAssistant = 2 // AI 助手(左侧显示) }; ``` +### 与 KBAiChatMessage 的映射关系 + +```objc +// KBChatHistoryModel → KBAiChatMessage +if (historyModel.sender == KBChatSenderUser) { + // sender = 1 → KBAiChatMessageTypeUser(右侧) + message = [KBAiChatMessage userMessageWithText:historyModel.content]; +} else if (historyModel.sender == KBChatSenderAssistant) { + // sender = 2 → KBAiChatMessageTypeAssistant(左侧) + message = [KBAiChatMessage assistantMessageWithText:historyModel.content]; +} +``` + --- ## ✅ 已完成功能 diff --git a/keyBoard/Class/AiTalk/V/KBChatTableView.h b/keyBoard/Class/AiTalk/V/KBChatTableView.h index 079f134..d47a58b 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTableView.h +++ b/keyBoard/Class/AiTalk/V/KBChatTableView.h @@ -6,14 +6,24 @@ // #import - -@class KBChatMessage; +#import "KBAiChatMessage.h" NS_ASSUME_NONNULL_BEGIN +@class KBChatTableView; + +@protocol KBChatTableViewDelegate +@optional +- (void)chatTableViewDidScroll:(KBChatTableView *)chatView + scrollView:(UIScrollView *)scrollView; +- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView; +@end + /// 聊天列表视图(支持用户消息、AI 消息、时间戳、语音播放) @interface KBChatTableView : UIView +@property (nonatomic, weak) id delegate; + /// 添加用户消息 - (void)addUserMessage:(NSString *)text; @@ -41,6 +51,21 @@ NS_ASSUME_NONNULL_BEGIN /// 停止正在播放的音频 - (void)stopPlayingAudio; +/// 结束加载更多 +- (void)endLoadMoreWithHasMoreData:(BOOL)hasMoreData; + +/// 重置无更多数据状态 +- (void)resetNoMoreData; + +/// 添加自定义消息(可用于历史消息或打字机) +- (void)addMessage:(KBAiChatMessage *)message + autoScroll:(BOOL)autoScroll; + +/// 用指定消息重载(用于历史消息分页) +- (void)reloadWithMessages:(NSArray *)messages + keepOffset:(BOOL)keepOffset + scrollToBottom:(BOOL)scrollToBottom; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBChatTableView.m b/keyBoard/Class/AiTalk/V/KBChatTableView.m index f5c4eb4..ce1178f 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/KBChatTableView.m @@ -11,6 +11,7 @@ #import "KBChatAssistantMessageCell.h" #import "KBChatTimeCell.h" #import "AiVM.h" +#import #import #import @@ -28,6 +29,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 @property (nonatomic, strong) AVAudioPlayer *audioPlayer; @property (nonatomic, strong) NSIndexPath *playingCellIndexPath; @property (nonatomic, strong) AiVM *aiVM; +@property (nonatomic, assign) BOOL hasMoreData; @end @@ -52,6 +54,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 - (void)setup { self.messages = [[NSMutableArray alloc] init]; self.aiVM = [[AiVM alloc] init]; + self.hasMoreData = YES; // 创建 TableView self.tableView = [[UITableView alloc] initWithFrame:self.bounds @@ -75,126 +78,54 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 // 布局 [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { - make.edges.equalTo(self); +// make.edges.equalTo(self); + make.top.left.right.equalTo(self); + make.bottom.equalTo(self).offset(-KB_TABBAR_HEIGHT - 40 - 10); }]; + + __weak typeof(self) weakSelf = self; + self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (!strongSelf.hasMoreData) { + [strongSelf.tableView.mj_footer endRefreshingWithNoMoreData]; + return; + } + + if ([strongSelf.delegate respondsToSelector:@selector(chatTableViewDidTriggerLoadMore:)]) { + [strongSelf.delegate chatTableViewDidTriggerLoadMore:strongSelf]; + } else { + [strongSelf.tableView.mj_footer endRefreshing]; + } + }]; + self.tableView.mj_footer.hidden = YES; } #pragma mark - Public Methods - (void)addUserMessage:(NSString *)text { - // 记录插入前的消息数量 - NSInteger oldCount = self.messages.count; - KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; - [self insertMessageWithTimestamp:message]; - - // 计算新增的行数 - NSInteger newCount = self.messages.count; - NSInteger insertedCount = newCount - oldCount; - - // 使用 insert 插入新行 - if (insertedCount > 0) { - NSMutableArray *indexPaths = [NSMutableArray array]; - for (NSInteger i = oldCount; i < newCount; i++) { - [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; - } - [self.tableView insertRowsAtIndexPaths:indexPaths - withRowAnimation:UITableViewRowAnimationNone]; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self scrollToBottom]; - }); + [self addMessage:message autoScroll:YES]; } - (void)addAssistantMessage:(NSString *)text audioDuration:(NSTimeInterval)duration audioData:(NSData *)audioData { - // 记录插入前的消息数量 - NSInteger oldCount = self.messages.count; - KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text audioDuration:duration audioData:audioData]; - [self insertMessageWithTimestamp:message]; - - // 计算新增的行数 - NSInteger newCount = self.messages.count; - NSInteger insertedCount = newCount - oldCount; - - // 使用 insert 插入新行 - if (insertedCount > 0) { - NSMutableArray *indexPaths = [NSMutableArray array]; - for (NSInteger i = oldCount; i < newCount; i++) { - [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; - } - [self.tableView insertRowsAtIndexPaths:indexPaths - withRowAnimation:UITableViewRowAnimationNone]; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self scrollToBottom]; - }); + [self addMessage:message autoScroll:YES]; } - (void)addAssistantMessage:(NSString *)text audioId:(NSString *)audioId { - NSLog(@"[KBChatTableView] ========== 添加新的 AI 消息 =========="); - NSLog(@"[KBChatTableView] 文本长度: %lu, audioId: %@", (unsigned long)text.length, audioId); - NSLog(@"[KBChatTableView] 当前消息数量: %ld", (long)self.messages.count); - - // 在添加新消息之前,先标记上一条 AI 消息完成,并停止其打字机效果 - for (NSInteger i = self.messages.count - 1; i >= 0; i--) { - KBAiChatMessage *msg = self.messages[i]; - if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) { - NSLog(@"[KBChatTableView] 找到上一条未完成的消息 - 索引: %ld, 文本: %@", (long)i, msg.text); - msg.isComplete = YES; - msg.needsTypewriterEffect = NO; - - // 停止该 Cell 的打字机效果 - NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0]; - KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath]; - if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) { - NSLog(@"[KBChatTableView] 停止上一条消息的打字机效果"); - [oldCell stopTypewriterEffect]; - // 显示完整文本 - oldCell.messageLabel.text = msg.text; - } - break; - } - } - - // 记录插入前的消息数量 - NSInteger oldCount = self.messages.count; - KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text audioId:audioId]; message.needsTypewriterEffect = YES; // 新消息需要打字机效果 - NSLog(@"[KBChatTableView] 新消息属性 - needsTypewriter: %d, isComplete: %d", - message.needsTypewriterEffect, message.isComplete); - [self insertMessageWithTimestamp:message]; - - // 计算新增的行数 - NSInteger newCount = self.messages.count; - NSInteger insertedCount = newCount - oldCount; - NSLog(@"[KBChatTableView] 插入后消息数量: %ld, 新增行数: %ld", (long)newCount, (long)insertedCount); - - // 使用 insert 插入新行 - if (insertedCount > 0) { - NSMutableArray *indexPaths = [NSMutableArray array]; - for (NSInteger i = oldCount; i < newCount; i++) { - [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; - NSLog(@"[KBChatTableView] 将插入行: %ld", (long)i); - } - [self.tableView insertRowsAtIndexPaths:indexPaths - withRowAnimation:UITableViewRowAnimationNone]; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self scrollToBottom]; - }); - - NSLog(@"[KBChatTableView] ========== 添加完成 =========="); + [self addMessage:message autoScroll:YES]; } - (void)updateLastAssistantMessage:(NSString *)text { @@ -245,6 +176,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 - (void)clearMessages { [self.messages removeAllObjects]; [self.tableView reloadData]; + [self updateFooterVisibility]; } - (void)scrollToBottom { @@ -257,6 +189,92 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 animated:YES]; } +#pragma mark - Public Helpers + +- (void)endLoadMoreWithHasMoreData:(BOOL)hasMoreData { + self.hasMoreData = hasMoreData; + if (hasMoreData) { + [self.tableView.mj_footer endRefreshing]; + } else { + [self.tableView.mj_footer endRefreshingWithNoMoreData]; + } + [self updateFooterVisibility]; +} + +- (void)resetNoMoreData { + self.hasMoreData = YES; + [self.tableView.mj_footer resetNoMoreData]; + [self updateFooterVisibility]; +} + +- (void)addMessage:(KBAiChatMessage *)message + autoScroll:(BOOL)autoScroll { + if (!message) { + return; + } + + if (message.type == KBAiChatMessageTypeAssistant && + message.needsTypewriterEffect && + !message.isComplete) { + [self stopPreviousIncompleteAssistantMessageIfNeeded]; + } + + NSInteger oldCount = self.messages.count; + [self insertMessageWithTimestamp:message]; + + NSInteger newCount = self.messages.count; + NSInteger insertedCount = newCount - oldCount; + + if (insertedCount > 0) { + NSMutableArray *indexPaths = [NSMutableArray array]; + for (NSInteger i = oldCount; i < newCount; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; + } + [self.tableView insertRowsAtIndexPaths:indexPaths + withRowAnimation:UITableViewRowAnimationNone]; + } + + [self updateFooterVisibility]; + + if (autoScroll) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self scrollToBottom]; + }); + } +} + +- (void)reloadWithMessages:(NSArray *)messages + keepOffset:(BOOL)keepOffset + scrollToBottom:(BOOL)scrollToBottom { + CGFloat oldContentHeight = self.tableView.contentSize.height; + CGFloat oldOffsetY = self.tableView.contentOffset.y; + + [self.messages removeAllObjects]; + if (messages.count > 0) { + for (KBAiChatMessage *message in messages) { + [self insertMessageWithTimestamp:message]; + } + } + + [self.tableView reloadData]; + [self.tableView layoutIfNeeded]; + [self updateFooterVisibility]; + + if (keepOffset) { + CGFloat newContentHeight = self.tableView.contentSize.height; + CGFloat delta = newContentHeight - oldContentHeight; + CGFloat offsetY = oldOffsetY + delta; + [self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO]; + return; + } + + if (scrollToBottom) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self scrollToBottom]; + }); + } +} + #pragma mark - Private Methods /// 插入消息并自动添加时间戳 @@ -323,6 +341,30 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 }); } +- (void)stopPreviousIncompleteAssistantMessageIfNeeded { + for (NSInteger i = self.messages.count - 1; i >= 0; i--) { + KBAiChatMessage *msg = self.messages[i]; + if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) { + msg.isComplete = YES; + msg.needsTypewriterEffect = NO; + + NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0]; + KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath]; + if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) { + [oldCell stopTypewriterEffect]; + oldCell.messageLabel.text = msg.text; + } + break; + } + } +} + +- (void)updateFooterVisibility { + BOOL canLoadMore = (self.delegate && + [self.delegate respondsToSelector:@selector(chatTableViewDidTriggerLoadMore:)]); + self.tableView.mj_footer.hidden = !canLoadMore || self.messages.count == 0; +} + #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { @@ -365,6 +407,19 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 } } +- (void)setDelegate:(id)delegate { + _delegate = delegate; + [self updateFooterVisibility]; +} + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if ([self.delegate respondsToSelector:@selector(chatTableViewDidScroll:scrollView:)]) { + [self.delegate chatTableViewDidScroll:self scrollView:scrollView]; + } +} + #pragma mark - KBChatAssistantMessageCellDelegate - (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h index 2e12e01..e9f3517 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h @@ -19,6 +19,13 @@ NS_ASSUME_NONNULL_BEGIN /// 预加载数据 - (void)preloadDataIfNeeded; +/// 添加用户消息 +- (void)appendUserMessage:(NSString *)text; + +/// 添加 AI 消息(支持打字机效果) +- (void)appendAssistantMessage:(NSString *)text + audioId:(nullable NSString *)audioId; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m index 8cf1ef0..4cc72a3 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m @@ -13,7 +13,7 @@ #import #import -@interface KBPersonaChatCell () +@interface KBPersonaChatCell () /// 背景图 @property (nonatomic, strong) UIImageView *backgroundImageView; @@ -28,7 +28,7 @@ @property (nonatomic, strong) UILabel *openingLabel; /// 聊天列表 -@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, strong) KBChatTableView *chatView; /// 聊天消息 @property (nonatomic, strong) NSMutableArray *messages; @@ -102,8 +102,8 @@ }]; // 聊天列表 - [self.contentView addSubview:self.tableView]; - [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { + [self.contentView addSubview:self.chatView]; + [self.chatView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.openingLabel.mas_bottom).offset(30); make.left.right.bottom.equalTo(self.contentView); }]; @@ -130,7 +130,7 @@ self.nameLabel.text = persona.name; self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText; - [self.tableView reloadData]; + [self.chatView clearMessages]; } #pragma mark - 2:数据加载 @@ -145,11 +145,16 @@ - (void)loadChatHistory { if (self.isLoading || !self.hasMoreHistory) { + [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } self.isLoading = YES; + if (self.currentPage == 1) { + [self.chatView resetNoMoreData]; + } + // 使用 persona.personaId 作为 companionId NSInteger companionId = self.persona.personaId; @@ -162,6 +167,7 @@ if (error) { NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); + [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; // 如果是第一次加载失败,显示开场白 if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) { @@ -177,12 +183,24 @@ NSMutableArray *newMessages = [NSMutableArray array]; for (KBChatHistoryModel *item in pageModel.records) { KBAiChatMessage *message; - if (item.isUserMessage) { + + // 根据 sender 判断消息类型 + // sender = 1: 用户消息(右侧) + // sender = 2: AI 消息(左侧) + if (item.sender == KBChatSenderUser) { + // 用户消息 message = [KBAiChatMessage userMessageWithText:item.content]; + } else if (item.sender == KBChatSenderAssistant) { + // AI 消息 + message = [KBAiChatMessage assistantMessageWithText:item.content]; } else { + // 未知类型,默认为 AI 消息 + NSLog(@"[KBPersonaChatCell] 未知的 sender 类型:%ld", (long)item.sender); message = [KBAiChatMessage assistantMessageWithText:item.content]; } + message.isComplete = YES; + message.needsTypewriterEffect = NO; [newMessages addObject:message]; } @@ -198,24 +216,12 @@ // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ - if (weakSelf.currentPage == 1) { - [weakSelf.tableView reloadData]; - - // 滚动到底部(最新消息) - if (weakSelf.messages.count > 0) { - NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:weakSelf.messages.count - 1 inSection:0]; - [weakSelf.tableView scrollToRowAtIndexPath:lastIndexPath - atScrollPosition:UITableViewScrollPositionBottom - animated:NO]; - } - } else { - // 保持滚动位置 - CGFloat oldContentHeight = weakSelf.tableView.contentSize.height; - [weakSelf.tableView reloadData]; - CGFloat newContentHeight = weakSelf.tableView.contentSize.height; - CGFloat offsetY = newContentHeight - oldContentHeight; - [weakSelf.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO]; - } + BOOL keepOffset = (weakSelf.currentPage != 1); + BOOL scrollToBottom = (weakSelf.currentPage == 1); + [weakSelf.chatView reloadWithMessages:weakSelf.messages + keepOffset:keepOffset + scrollToBottom:scrollToBottom]; + [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; }); NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", @@ -227,6 +233,7 @@ - (void)loadMoreHistory { if (!self.hasMoreHistory || self.isLoading) { + [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } @@ -238,41 +245,53 @@ // 显示开场白作为第一条消息 KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; openingMsg.isComplete = YES; + openingMsg.needsTypewriterEffect = NO; [self.messages addObject:openingMsg]; dispatch_async(dispatch_get_main_queue(), ^{ - [self.tableView reloadData]; + [self.chatView reloadWithMessages:self.messages + keepOffset:NO + scrollToBottom:YES]; }); } -#pragma mark - UITableViewDataSource +#pragma mark - 3:消息追加 -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return self.messages.count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; - if (!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"]; - cell.backgroundColor = [UIColor clearColor]; - cell.textLabel.textColor = [UIColor whiteColor]; - cell.textLabel.numberOfLines = 0; +- (void)appendUserMessage:(NSString *)text { + if (text.length == 0) { + return; } - KBAiChatMessage *message = self.messages[indexPath.row]; - cell.textLabel.text = message.text; + if (!self.messages) { + self.messages = [NSMutableArray array]; + } - return cell; + KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; + [self.messages addObject:message]; + [self.chatView addMessage:message autoScroll:YES]; } -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return UITableViewAutomaticDimension; +- (void)appendAssistantMessage:(NSString *)text + audioId:(NSString *)audioId { + if (text.length == 0) { + return; + } + + if (!self.messages) { + self.messages = [NSMutableArray array]; + } + + KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text + audioId:audioId]; + message.needsTypewriterEffect = YES; + [self.messages addObject:message]; + [self.chatView addMessage:message autoScroll:YES]; } -#pragma mark - UIScrollViewDelegate +#pragma mark - KBChatTableViewDelegate -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { +- (void)chatTableViewDidScroll:(KBChatTableView *)chatView + scrollView:(UIScrollView *)scrollView { CGFloat offsetY = scrollView.contentOffset.y; // 下拉到顶部,加载历史消息 @@ -281,6 +300,10 @@ } } +- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView { + [self loadMoreHistory]; +} + #pragma mark - Lazy Load - (UIImageView *)backgroundImageView { @@ -325,22 +348,13 @@ return _openingLabel; } -- (UITableView *)tableView { - if (!_tableView) { - _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - _tableView.delegate = self; - _tableView.dataSource = self; - _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; - _tableView.backgroundColor = [UIColor clearColor]; - _tableView.showsVerticalScrollIndicator = NO; - _tableView.estimatedRowHeight = 60; - _tableView.rowHeight = UITableViewAutomaticDimension; - - if (@available(iOS 11.0, *)) { - _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } +- (KBChatTableView *)chatView { + if (!_chatView) { + _chatView = [[KBChatTableView alloc] init]; + _chatView.backgroundColor = [UIColor clearColor]; + _chatView.delegate = self; } - return _tableView; + return _chatView; } @end diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index 0235fa2..a58a60e 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -81,7 +81,7 @@ [self.view addSubview:self.voiceInputBar]; [self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); - make.bottom.equalTo(self.view); + make.bottom.equalTo(self.view).offset(-20); make.height.mas_equalTo(150); // 根据实际需要调整高度 }]; } @@ -267,6 +267,27 @@ return persona.personaId; } +- (KBPersonaChatCell *)currentPersonaCell { + if (self.personas.count == 0) { + return nil; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0]; + KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + if (cell) { + return cell; + } + + for (NSIndexPath *visibleIndex in self.collectionView.indexPathsForVisibleItems) { + KBPersonaChatCell *visibleCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:visibleIndex]; + if (visibleCell) { + return visibleCell; + } + } + + return nil; +} + #pragma mark - Lazy Load - (UICollectionView *)collectionView { @@ -315,14 +336,43 @@ return; } + KBPersonaChatCell *currentCell = [self currentPersonaCell]; + if (currentCell) { + [currentCell appendUserMessage:text]; + } + + __weak typeof(self) weakSelf = self; [self.aiVM requestChatMessageWithContent:text companionId:companionId completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) { - if (error) { - NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { return; } - NSLog(@"[KBAIHomeVC] 聊天请求成功,code=%ld", (long)response.code); + + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); + return; + } + + if (!response || !response.data) { + NSLog(@"[KBAIHomeVC] 聊天响应为空"); + return; + } + + NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @""; + NSString *audioId = response.data.audioId; + if (aiResponse.length == 0) { + NSLog(@"[KBAIHomeVC] AI 回复为空"); + return; + } + + KBPersonaChatCell *cell = [strongSelf currentPersonaCell]; + if (cell) { + [cell appendAssistantMessage:aiResponse audioId:audioId]; + } + }); }]; }