diff --git a/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m index 82ba72b..6457492 100644 --- a/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m +++ b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m @@ -40,12 +40,12 @@ make.height.mas_equalTo(24); }]; - [self.lineView mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.contentView).offset(16); - make.right.equalTo(self.contentView).offset(-16); - make.bottom.equalTo(self.contentView); - make.height.mas_equalTo(0.5); - }]; +// [self.lineView mas_makeConstraints:^(MASConstraintMaker *make) { +// make.left.equalTo(self.contentView).offset(16); +// make.right.equalTo(self.contentView).offset(-16); +// make.bottom.equalTo(self.contentView); +// make.height.mas_equalTo(0.5); +// }]; } #pragma mark - Configuration diff --git a/keyBoard/Class/AiTalk/V/KBAICommentView.m b/keyBoard/Class/AiTalk/V/KBAICommentView.m index 4c9058d..91b8048 100644 --- a/keyBoard/Class/AiTalk/V/KBAICommentView.m +++ b/keyBoard/Class/AiTalk/V/KBAICommentView.m @@ -16,6 +16,7 @@ #import "AiVM.h" #import #import +#import static NSString *const kCommentHeaderIdentifier = @"CommentHeader"; static NSString *const kReplyCellIdentifier = @"ReplyCell"; @@ -33,6 +34,12 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; @property(nonatomic, strong) NSMutableArray *comments; @property(nonatomic, assign) NSInteger totalCommentCount; +/// 分页参数 +@property(nonatomic, assign) NSInteger currentPage; +@property(nonatomic, assign) NSInteger pageSize; +@property(nonatomic, assign) BOOL isLoading; +@property(nonatomic, assign) BOOL hasMoreData; + /// 键盘高度 @property(nonatomic, assign) CGFloat keyboardHeight; /// 输入框底部约束 @@ -56,6 +63,9 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; self = [super initWithFrame:frame]; if (self) { self.comments = [NSMutableArray array]; + self.currentPage = 1; + self.pageSize = 20; + self.hasMoreData = YES; [self setupUI]; [self setupKeyboardObservers]; } @@ -114,6 +124,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; make.left.right.equalTo(self); make.bottom.equalTo(self.inputView.mas_top); }]; + + // 上拉加载更多 + __weak typeof(self) weakSelf = self; + MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf loadMoreComments]; + }]; + footer.stateLabel.hidden = YES; + footer.backgroundColor = [UIColor clearColor]; + footer.automaticallyHidden = YES; + self.tableView.mj_footer = footer; } #pragma mark - Keyboard Observers @@ -174,49 +198,87 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; #pragma mark - Data Loading - (void)loadComments { + if (self.isLoading) { + return; + } + + self.currentPage = 1; + self.hasMoreData = YES; + [self.tableView.mj_footer resetNoMoreData]; + + [self fetchCommentsAtPage:self.currentPage append:NO]; +} + +- (void)loadMoreComments { + if (self.isLoading) { + [self.tableView.mj_footer endRefreshing]; + return; + } + + if (!self.hasMoreData) { + [self.tableView.mj_footer endRefreshingWithNoMoreData]; + return; + } + + NSInteger nextPage = self.currentPage + 1; + [self fetchCommentsAtPage:nextPage append:YES]; +} + +- (void)fetchCommentsAtPage:(NSInteger)page append:(BOOL)append { if (self.companionId <= 0) { NSLog(@"[KBAICommentView] companionId 未设置,无法加载评论"); [self showEmptyState]; + [self.tableView.mj_footer endRefreshing]; return; } + + self.isLoading = YES; __weak typeof(self) weakSelf = self; [self.aiVM fetchCommentsWithCompanionId:self.companionId - pageNum:1 - pageSize:20 + pageNum:page + pageSize:self.pageSize completion:^(KBCommentPageModel *pageModel, NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } + + strongSelf.isLoading = NO; if (error) { NSLog(@"[KBAICommentView] 加载评论失败:%@", error.localizedDescription); - // 加载失败也显示空态 dispatch_async(dispatch_get_main_queue(), ^{ - [strongSelf showEmptyStateWithError]; + if (append) { + [strongSelf.tableView.mj_footer endRefreshing]; + } else { + [strongSelf showEmptyStateWithError]; + } }); return; } dispatch_async(dispatch_get_main_queue(), ^{ - [strongSelf updateCommentsWithPageModel:pageModel]; + [strongSelf updateCommentsWithPageModel:pageModel append:append]; }); }]; } /// 更新评论数据(从后端返回的 KBCommentPageModel 转换为 UI 层的 KBAICommentModel) -- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel { +- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel append:(BOOL)append { if (!pageModel) { NSLog(@"[KBAICommentView] pageModel 为空"); // 数据为空,显示空态 [self showEmptyState]; + [self.tableView.mj_footer endRefreshing]; return; } self.totalCommentCount = pageModel.total; - - [self.comments removeAllObjects]; + + if (!append) { + [self.comments removeAllObjects]; + } // 获取 tableView 宽度用于计算高度 CGFloat tableWidth = self.tableView.bounds.size.width; @@ -224,7 +286,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; tableWidth = [UIScreen mainScreen].bounds.size.width; } - NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条", (long)pageModel.records.count, (long)pageModel.total); + NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条,页码:%ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages); for (KBCommentItem *item in pageModel.records) { // 转换为 KBAICommentModel(使用 MJExtension) @@ -243,6 +305,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; [self updateTitle]; [self.tableView reloadData]; + + // 更新分页状态 + self.currentPage = pageModel.current > 0 ? pageModel.current : self.currentPage; + if (pageModel.pages > 0) { + self.hasMoreData = pageModel.current < pageModel.pages; + } else { + self.hasMoreData = pageModel.records.count >= self.pageSize; + } + + if (self.hasMoreData) { + [self.tableView.mj_footer endRefreshing]; + } else { + [self.tableView.mj_footer endRefreshingWithNoMoreData]; + } // 根据数据是否为空,动态控制空态显示 if (self.comments.count == 0) { @@ -641,6 +717,7 @@ static NSInteger const kRepliesLoadCount = 5; _tableView.backgroundColor = [UIColor clearColor]; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; _tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; + _tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 0.01)]; // 关闭空数据占位,避免加载时显示"暂无数据" _tableView.useEmptyDataSet = NO; diff --git a/keyBoard/Class/AiTalk/V/KBAIReplyCell.m b/keyBoard/Class/AiTalk/V/KBAIReplyCell.m index 8234e03..7e25982 100644 --- a/keyBoard/Class/AiTalk/V/KBAIReplyCell.m +++ b/keyBoard/Class/AiTalk/V/KBAIReplyCell.m @@ -51,7 +51,7 @@ [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(68); // 16 + 40 + 12 = 68 make.top.equalTo(self.contentView).offset(8); - make.width.height.mas_equalTo(28); + make.width.height.mas_equalTo(26); }]; [self.userNameLabel mas_makeConstraints:^(MASConstraintMaker *make) { @@ -171,7 +171,7 @@ if (!_avatarImageView) { _avatarImageView = [[UIImageView alloc] init]; _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; - _avatarImageView.layer.cornerRadius = 14; + _avatarImageView.layer.cornerRadius = 13; _avatarImageView.layer.masksToBounds = YES; _avatarImageView.backgroundColor = [UIColor systemGray5Color]; } @@ -182,7 +182,7 @@ if (!_userNameLabel) { _userNameLabel = [[UILabel alloc] init]; _userNameLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium]; - _userNameLabel.textColor = [UIColor secondaryLabelColor]; + _userNameLabel.textColor = [UIColor colorWithHex:0x9F9F9F]; _userNameLabel.numberOfLines = 0; } return _userNameLabel; @@ -191,8 +191,8 @@ - (UILabel *)contentLabel { if (!_contentLabel) { _contentLabel = [[UILabel alloc] init]; - _contentLabel.font = [UIFont systemFontOfSize:14]; - _contentLabel.textColor = [UIColor labelColor]; + _contentLabel.font = [UIFont systemFontOfSize:12]; + _contentLabel.textColor = [UIColor whiteColor]; _contentLabel.numberOfLines = 0; } return _contentLabel; @@ -201,8 +201,8 @@ - (UILabel *)timeLabel { if (!_timeLabel) { _timeLabel = [[UILabel alloc] init]; - _timeLabel.font = [UIFont systemFontOfSize:11]; - _timeLabel.textColor = [UIColor secondaryLabelColor]; + _timeLabel.font = [UIFont systemFontOfSize:12]; + _timeLabel.textColor = [UIColor colorWithHex:0x9F9F9F]; } return _timeLabel; } diff --git a/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m b/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m index 965c688..0eea5cc 100644 --- a/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m +++ b/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m @@ -83,7 +83,8 @@ [self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(16); make.top.equalTo(self.contentView).offset(8); - make.width.height.mas_equalTo(24); +// make.width.height.mas_equalTo(24); + make.width.height.mas_equalTo(0); }]; [self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) { @@ -105,8 +106,8 @@ // 关键修复:messageLabel 约束必须完整,让 AutoLayout 能推导出 bubbleView 的高度 [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.bubbleView).offset(10); - make.bottom.equalTo(self.bubbleView).offset(-10).priority(999); // 降低优先级 + make.top.equalTo(self.bubbleView).offset(5); + make.bottom.equalTo(self.bubbleView).offset(-5).priority(999); // 降低优先级 make.left.equalTo(self.bubbleView).offset(12); make.right.equalTo(self.bubbleView).offset(-12); // 关键修复:给 messageLabel 一个最小高度,防止高度为 0 diff --git a/keyBoard/Class/AiTalk/V/KBChatTimeCell.m b/keyBoard/Class/AiTalk/V/KBChatTimeCell.m index 0fb219c..df3b34e 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTimeCell.m +++ b/keyBoard/Class/AiTalk/V/KBChatTimeCell.m @@ -38,8 +38,8 @@ // 布局约束 [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.contentView).offset(8); - make.bottom.equalTo(self.contentView).offset(-8); + make.top.equalTo(self.contentView).offset(1); + make.bottom.equalTo(self.contentView).offset(-1); make.centerX.equalTo(self.contentView); }]; } diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m index de82b49..0e59354 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m @@ -202,8 +202,14 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe // 关键修复:清空消息时停止音频播放,避免状态混乱 [self.chatView stopPlayingAudio]; - // 如果有缓存,直接显示 + // 确保开场白在第一条 + [self ensureOpeningMessageAtTop]; + + // 如果有消息,直接显示(包含开场白) if (self.messages.count > 0) { + // 同步缓存,避免下次从缓存缺少开场白 + [[KBAIChatMessageCacheManager shared] saveMessages:self.messages + forCompanionId:persona.personaId]; [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:YES]; @@ -287,14 +293,19 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe [newMessages addObject:message]; } - // 插入到顶部(历史消息) + // 插入历史消息(确保开场白始终是第一条) if (weakSelf.currentPage == 1) { // 第一页,直接赋值 weakSelf.messages = newMessages; + [weakSelf ensureOpeningMessageAtTop]; } else { - // 后续页,插入到顶部 - NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)]; - [weakSelf.messages insertObjects:newMessages atIndexes:indexSet]; + // 后续页,插入到开场白之后 + [weakSelf ensureOpeningMessageAtTop]; + if (newMessages.count > 0) { + NSUInteger insertIndex = [weakSelf hasOpeningMessageAtTop] ? 1 : 0; + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)]; + [weakSelf.messages insertObjects:newMessages atIndexes:indexSet]; + } } // 刷新 UI @@ -306,8 +317,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe scrollToBottom:scrollToBottom]; [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; - // ✅ 保存到缓存 - [[KBAIChatMessageCacheManager shared] saveMessages:weakSelf.messages + // ✅ 保存到缓存(包含开场白) + [[KBAIChatMessageCacheManager shared] saveMessages:weakSelf.messages forCompanionId:companionId]; }); @@ -332,10 +343,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe - (void)showOpeningMessage { // 显示开场白作为第一条消息 - KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; - openingMsg.isComplete = YES; - openingMsg.needsTypewriterEffect = NO; - [self.messages addObject:openingMsg]; + [self ensureOpeningMessageAtTop]; dispatch_async(dispatch_get_main_queue(), ^{ [self.chatView reloadWithMessages:self.messages @@ -344,6 +352,41 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe }); } +- (BOOL)hasOpeningMessageAtTop { + if (self.messages.count == 0) { + return NO; + } + return [self isOpeningMessage:self.messages.firstObject]; +} + +- (BOOL)isOpeningMessage:(KBAiChatMessage *)message { + if (!message) { + return NO; + } + NSString *introText = self.persona.introText ?: @""; + if (introText.length == 0) { + return NO; + } + return (message.type == KBAiChatMessageTypeAssistant) && [message.text isEqualToString:introText]; +} + +- (void)ensureOpeningMessageAtTop { + NSString *introText = self.persona.introText ?: @""; + if (introText.length == 0) { + return; + } + if (!self.messages) { + self.messages = [NSMutableArray array]; + } + if ([self hasOpeningMessageAtTop]) { + return; + } + KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:introText]; + openingMsg.isComplete = YES; + openingMsg.needsTypewriterEffect = NO; + [self.messages insertObject:openingMsg atIndex:0]; +} + #pragma mark - 通知处理 /// 处理聊天会话被重置的通知 @@ -368,10 +411,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe // 清空聊天视图 [self.chatView clearMessages]; - // 显示开场白 - if (self.persona.introText.length > 0) { - [self showOpeningMessage]; - } + // 显示开场白(始终保持第一条) + [self showOpeningMessage]; } } @@ -386,13 +427,14 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe self.messages = [NSMutableArray array]; } + [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; [self.messages addObject:message]; [self.chatView addMessage:message autoScroll:YES]; } - (void)appendAssistantMessage:(NSString *)text - audioId:(NSString *)audioId { + audioId:(NSString *)audioId { if (text.length == 0) { return; } @@ -401,6 +443,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe self.messages = [NSMutableArray array]; } + [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text audioId:audioId]; message.needsTypewriterEffect = YES; diff --git a/keyBoard/Class/AiTalk/VM/AiVM.m b/keyBoard/Class/AiTalk/VM/AiVM.m index e040ab7..94d21c8 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.m +++ b/keyBoard/Class/AiTalk/VM/AiVM.m @@ -242,7 +242,7 @@ autoShowBusinessError:NO [[KBNetworkManager shared] uploadFile:API_AI_AUDIO_UPLOAD fileURL:fileURL name:@"file" - mimeType:@"audio/mp4" + mimeType:@"audio/m4a" parameters:nil headers:nil completion:^(NSDictionary *_Nullable json,