// // KBPersonaChatCell.m // keyBoard // // Created by Kiro on 2026/1/26. // #import "KBPersonaChatCell.h" #import "KBAiChatMessage.h" #import "KBChatHistoryPageModel.h" #import "AiVM.h" #import "KBImagePositionButton.h" #import "KBAICommentView.h" #import "KBAIChatMessageCacheManager.h" #import #import #import #import "AIPersonInfoVC.h" /// 聊天会话被重置的通知 static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification"; @interface KBPersonaChatCell () /// 背景图 @property (nonatomic, strong) UIImageView *backgroundImageView; /// 头像 @property (nonatomic, strong) UIImageView *avatarImageView; /// 人设名称 @property (nonatomic, strong) UILabel *nameLabel; /// 开场白 @property (nonatomic, strong) UILabel *openingLabel; /// 聊天消息 @property (nonatomic, strong) NSMutableArray *messages; /// 是否已加载数据 @property (nonatomic, assign) BOOL hasLoadedData; /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; @property (nonatomic, assign) BOOL canTriggerLoadMore; /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; /// 是否还有更多历史消息 @property (nonatomic, assign) BOOL hasMoreHistory; /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; /// 评论按钮 @property (nonatomic, strong) KBImagePositionButton *commentButton; /// 喜欢按钮 @property (nonatomic, strong) KBImagePositionButton *likeButton; /// 评论弹窗 @property (nonatomic, weak) LSTPopView *popView; @property (nonatomic, strong) NSMutableDictionary *pendingAssistantMessages; @property (nonatomic, assign) BOOL isCurrentPersonaCell; @property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio; @property (nonatomic, assign) BOOL hasPlayedPrologueAudio; @end @implementation KBPersonaChatCell #pragma mark - Lifecycle - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupUI]; // 监听聊天会话重置通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleChatSessionReset:) name:KBChatSessionDidResetNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } /// 关键修复:Cell 复用时不清空数据,避免重复请求 - (void)prepareForReuse { [super prepareForReuse]; // 停止音频播放 [self.chatView stopPlayingAudio]; // 重置加载状态标志(但不清空 hasLoadedData) self.isLoading = NO; self.canTriggerLoadMore = YES; [self.pendingAssistantMessages removeAllObjects]; self.isCurrentPersonaCell = NO; self.shouldAutoPlayPrologueAudio = NO; self.hasPlayedPrologueAudio = NO; // ✅ 移除了 self.hasLoadedData = NO; // 这样 Cell 复用时不会重复请求数据 } #pragma mark - 1:控件初始化 - (void)setupUI { // 背景图 [self.contentView addSubview:self.backgroundImageView]; [self.backgroundImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; // 半透明遮罩 UIView *maskView = [[UIView alloc] init]; maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3]; [self.contentView addSubview:maskView]; [maskView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; // 开场白 [self.contentView addSubview:self.openingLabel]; [self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT); make.left.equalTo(self.contentView).offset(40); make.right.equalTo(self.contentView).offset(-40); }]; // 头像 [self.contentView addSubview:self.avatarImageView]; [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(self.contentView).offset(-KB_TABBAR_HEIGHT - 50 - 20); make.left.equalTo(self.contentView).offset(20); make.size.mas_equalTo(CGSizeMake(54, 54)); }]; // 人设名称 [self.contentView addSubview:self.nameLabel]; [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.avatarImageView.mas_right).offset(5); make.centerY.equalTo(self.avatarImageView); }]; // 评论按钮(最右侧) [self.contentView addSubview:self.commentButton]; [self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.contentView).offset(-20); make.centerY.equalTo(self.avatarImageView); make.width.mas_equalTo(40); make.height.mas_equalTo(50); }]; // 喜欢按钮(评论按钮左侧,间距20px) [self.contentView addSubview:self.likeButton]; [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.commentButton.mas_left).offset(-20); make.centerY.equalTo(self.avatarImageView); make.width.mas_equalTo(40); make.height.mas_equalTo(50); }]; // 聊天列表 [self.contentView addSubview:self.chatView]; [self.chatView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT); make.left.right.equalTo(self.contentView); make.bottom.equalTo(self.avatarImageView.mas_top).offset(-10); }]; } #pragma mark - Setter - (NSString *)currentPrologueText { if (self.persona.prologue.length > 0) { return self.persona.prologue; } return self.persona.introText ?: @""; } - (void)setPersona:(KBPersonaModel *)persona { _persona = persona; // 重置状态 self.isLoading = NO; self.canTriggerLoadMore = YES; self.currentPage = 1; self.hasMoreHistory = YES; [self.pendingAssistantMessages removeAllObjects]; self.isCurrentPersonaCell = NO; self.shouldAutoPlayPrologueAudio = NO; self.hasPlayedPrologueAudio = NO; // ⚠️ 临时禁用缓存,排查问题 // NSArray *cachedMessages = [[KBAIChatMessageCacheManager shared] messagesForCompanionId:persona.personaId]; // if (cachedMessages.count > 0) { // self.messages = [cachedMessages mutableCopy]; // self.hasLoadedData = YES; // NSLog(@"[Cell] ✅ 从缓存加载:personaId=%ld, 消息数=%ld", (long)persona.personaId, (long)cachedMessages.count); // } else { self.messages = [NSMutableArray array]; self.hasLoadedData = NO; NSLog(@"[Cell] ⚠️ 缓存已禁用:personaId=%ld, 需要请求数据", (long)persona.personaId); // } // 设置 UI [self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl] placeholderImage:[UIImage imageNamed:@"placeholder_bg"]]; [self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl] placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]]; self.nameLabel.text = persona.name; self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.prologue; // 关键修复:清空消息时停止音频播放,避免状态混乱 [self.chatView stopPlayingAudio]; NSLog(@"[KBPersonaChatCell] ========== setPersona 调试 =========="); NSLog(@"[KBPersonaChatCell] personaId: %ld", (long)persona.personaId); NSLog(@"[KBPersonaChatCell] messages.count: %ld", (long)self.messages.count); NSLog(@"[KBPersonaChatCell] chatView.frame: %@", NSStringFromCGRect(self.chatView.frame)); NSLog(@"[KBPersonaChatCell] contentView.frame: %@", NSStringFromCGRect(self.contentView.frame)); if (self.messages.count > 0) { [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; // 同步缓存,避免下次从缓存缺少开场白 [[KBAIChatMessageCacheManager shared] saveMessages:self.messages forCompanionId:persona.personaId]; [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:YES]; } else { [self.chatView clearMessages]; [self.chatView updateIntroFooterText:persona.prologue]; } NSLog(@"[KBPersonaChatCell] ========== setPersona 结束 =========="); [self.commentButton setTitle:persona.commentCount forState:UIControlStateNormal]; [self.likeButton setTitle:persona.likeCount forState:UIControlStateNormal]; self.likeButton.selected = persona.liked; } #pragma mark - 2:数据加载 - (void)preloadDataIfNeeded { if (self.hasLoadedData || self.isLoading) { return; } [self loadChatHistory]; } - (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; __weak typeof(self) weakSelf = self; [self.aiVM fetchChatHistoryWithCompanionId:companionId pageNum:self.currentPage pageSize:10 completion:^(KBChatHistoryPageModel *pageModel, NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } if (error) { NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); dispatch_async(dispatch_get_main_queue(), ^{ strongSelf.isLoading = NO; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; if (strongSelf.currentPage == 1 && strongSelf.persona.prologue.length > 0) { [strongSelf showOpeningMessage]; } }); return; } strongSelf.hasLoadedData = YES; strongSelf.hasMoreHistory = pageModel.hasMore; NSInteger loadedPage = strongSelf.currentPage; if (loadedPage == 1) { BOOL isEmpty = (pageModel.total == 0); strongSelf.shouldAutoPlayPrologueAudio = isEmpty && (strongSelf.persona.prologueAudio.length > 0); if (!strongSelf.shouldAutoPlayPrologueAudio) { [strongSelf.chatView stopPlayingAudio]; } else { [strongSelf tryPlayPrologueAudioIfNeeded]; } } if (loadedPage == 1 && pageModel.total == 0) { dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf.chatView clearMessages]; [strongSelf.chatView updateIntroFooterText:strongSelf.persona.prologue]; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; strongSelf.isLoading = NO; }); strongSelf.currentPage++; return; } // 转换为 KBAiChatMessage NSMutableArray *newMessages = [NSMutableArray array]; for (KBChatHistoryModel *item in pageModel.records) { KBAiChatMessage *message; // 根据 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]; // [newMessages insertObject:message atIndex:0]; } // 插入历史消息(确保开场白始终是第一条) [strongSelf.chatView updateIntroFooterText:nil]; if (loadedPage == 1) { // 第一页,直接赋值 strongSelf.messages = newMessages; [strongSelf ensureOpeningMessageAtTop]; } else { // 后续页,继续加载历史 [strongSelf ensureOpeningMessageAtTop]; if (newMessages.count > 0) { if (strongSelf.chatView.inverted) { NSInteger openingIndex = [strongSelf openingMessageIndexInMessages]; NSUInteger insertIndex = (openingIndex != NSNotFound) ? (NSUInteger)openingIndex : strongSelf.messages.count; NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)]; [strongSelf.messages insertObjects:newMessages atIndexes:indexSet]; } else { NSUInteger insertIndex = [strongSelf hasOpeningMessageAtTop] ? 1 : 0; NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)]; [strongSelf.messages insertObjects:newMessages atIndexes:indexSet]; } } } // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ if (loadedPage == 1) { NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: 0, scrollToBottom: 1", (long)loadedPage); [strongSelf.chatView reloadWithMessages:strongSelf.messages keepOffset:NO scrollToBottom:YES]; } else { if (strongSelf.chatView.inverted) { NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, appendHistory", (long)loadedPage); KBAiChatMessage *openingMessage = [strongSelf openingMessageInMessages]; [strongSelf.chatView appendHistoryMessages:newMessages openingMessage:openingMessage]; } else { NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, prependHistory", (long)loadedPage); KBAiChatMessage *openingMessage = [strongSelf hasOpeningMessageAtTop] ? strongSelf.messages.firstObject : nil; [strongSelf.chatView prependHistoryMessages:newMessages openingMessage:openingMessage]; } } [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; // ✅ 保存到缓存(包含开场白) [[KBAIChatMessageCacheManager shared] saveMessages:strongSelf.messages forCompanionId:companionId]; strongSelf.isLoading = NO; }); strongSelf.currentPage++; NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", (long)strongSelf.currentPage - 1, (long)newMessages.count, pageModel.hasMore ? @"是" : @"否"); }]; } #pragma mark - Prologue Audio - (void)tryPlayPrologueAudioIfNeeded { if (!self.isCurrentPersonaCell) { return; } if (!self.shouldAutoPlayPrologueAudio) { return; } if (self.hasPlayedPrologueAudio) { return; } if (self.persona.prologueAudio.length == 0) { return; } self.hasPlayedPrologueAudio = YES; [self.chatView playRemoteAudioWithURLString:self.persona.prologueAudio]; } - (void)onBecameCurrentPersonaCell { self.isCurrentPersonaCell = YES; [self tryPlayPrologueAudioIfNeeded]; } - (void)onResignedCurrentPersonaCell { self.isCurrentPersonaCell = NO; [self.chatView stopPlayingAudio]; } - (void)loadMoreHistory { if (!self.hasMoreHistory || self.isLoading) { [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } [self loadChatHistory]; } - (void)showOpeningMessage { if (self.messages.count == 0) { [self.chatView clearMessages]; [self.chatView updateIntroFooterText:self.persona.prologue]; return; } [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; dispatch_async(dispatch_get_main_queue(), ^{ [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:YES]; }); } - (BOOL)hasOpeningMessageAtTop { if (self.messages.count == 0) { return NO; } if (self.chatView.inverted) { return [self isOpeningMessage:self.messages.lastObject]; } return [self isOpeningMessage:self.messages.firstObject]; } - (BOOL)isOpeningMessage:(KBAiChatMessage *)message { if (!message) { return NO; } NSString *prologue = [self currentPrologueText]; if (prologue.length == 0) { return NO; } return (message.type == KBAiChatMessageTypeAssistant) && [message.text isEqualToString:prologue]; } - (void)ensureOpeningMessageAtTop { NSString *prologue = [self currentPrologueText]; if (prologue.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } if ([self hasOpeningMessageAtTop]) { return; } KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:prologue]; openingMsg.isComplete = YES; openingMsg.needsTypewriterEffect = NO; if (self.chatView.inverted) { [self.messages addObject:openingMsg]; } else { [self.messages insertObject:openingMsg atIndex:0]; } } - (nullable KBAiChatMessage *)openingMessageInMessages { NSInteger index = [self openingMessageIndexInMessages]; if (index == NSNotFound) { return nil; } return self.messages[index]; } - (NSInteger)openingMessageIndexInMessages { NSString *prologue = [self currentPrologueText]; if (prologue.length == 0 || self.messages.count == 0) { return NSNotFound; } if (self.chatView.inverted) { NSInteger lastIndex = self.messages.count - 1; KBAiChatMessage *msg = self.messages[lastIndex]; return [self isOpeningMessage:msg] ? lastIndex : NSNotFound; } KBAiChatMessage *first = self.messages.firstObject; return [self isOpeningMessage:first] ? 0 : NSNotFound; } #pragma mark - 通知处理 /// 处理聊天会话被重置的通知 - (void)handleChatSessionReset:(NSNotification *)notification { NSNumber *companionIdObj = notification.userInfo[@"companionId"]; if (!companionIdObj) { return; } NSInteger companionId = [companionIdObj integerValue]; // 如果是当前显示的人设,清空聊天记录 if (self.persona && self.persona.personaId == companionId) { NSLog(@"[KBPersonaChatCell] 收到聊天重置通知:companionId=%ld, 清空聊天记录", (long)companionId); // 清空消息数组 self.messages = [NSMutableArray array]; self.hasLoadedData = NO; self.currentPage = 1; self.hasMoreHistory = YES; // 清空聊天视图 [self.chatView clearMessages]; // 显示开场白(始终保持第一条) [self showOpeningMessage]; } } #pragma mark - 3:消息追加 - (void)appendUserMessage:(NSString *)text { if (text.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } self.shouldAutoPlayPrologueAudio = NO; [self.chatView stopPlayingAudio]; [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; if (self.chatView.inverted) { [self.messages insertObject:message atIndex:0]; } else { [self.messages addObject:message]; } [self.chatView addMessage:message autoScroll:YES]; } - (void)appendUserMessage:(NSString *)text requestId:(NSString *)requestId { [self appendUserMessage:text]; } - (void)appendLoadingUserMessage { if (!self.messages) { self.messages = [NSMutableArray array]; } self.shouldAutoPlayPrologueAudio = NO; [self.chatView stopPlayingAudio]; [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage]; if (self.chatView.inverted) { [self.messages insertObject:message atIndex:0]; } else { [self.messages addObject:message]; } [self.chatView addMessage:message autoScroll:YES]; } - (void)updateLastUserMessage:(NSString *)text { [self.chatView updateLastUserMessage:text]; // 更新数据源中的消息 if (self.chatView.inverted) { for (NSInteger i = 0; i < self.messages.count; i++) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeUser && message.isLoading) { message.text = text; message.isLoading = NO; message.isComplete = YES; break; } } } else { for (NSInteger i = self.messages.count - 1; i >= 0; i--) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeUser && message.isLoading) { message.text = text; message.isLoading = NO; message.isComplete = YES; break; } } } } - (void)markLastUserMessageLoadingComplete { [self.chatView markLastUserMessageLoadingComplete]; // 同步更新数据源 if (self.chatView.inverted) { for (NSInteger i = 0; i < self.messages.count; i++) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeUser && message.isLoading) { message.isLoading = NO; break; } } } else { for (NSInteger i = self.messages.count - 1; i >= 0; i--) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeUser && message.isLoading) { message.isLoading = NO; break; } } } } - (void)appendAssistantMessage:(NSString *)text audioId:(NSString *)audioId { if (text.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } self.shouldAutoPlayPrologueAudio = NO; [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; // 查找并移除 loading 消息 [self removeLoadingAssistantMessage]; KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text audioId:audioId]; message.needsTypewriterEffect = YES; if (self.chatView.inverted) { [self.messages insertObject:message atIndex:0]; } else { [self.messages addObject:message]; } [self.chatView addMessage:message autoScroll:YES]; } /// 添加 loading AI 消息 - (void)appendLoadingAssistantMessage { if (!self.messages) { self.messages = [NSMutableArray array]; } self.shouldAutoPlayPrologueAudio = NO; [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage]; if (self.chatView.inverted) { [self.messages insertObject:message atIndex:0]; } else { [self.messages addObject:message]; } [self.chatView addMessage:message autoScroll:YES]; } - (void)appendLoadingAssistantMessageWithRequestId:(NSString *)requestId { if (requestId.length == 0) { [self appendLoadingAssistantMessage]; return; } if (!self.pendingAssistantMessages) { self.pendingAssistantMessages = [NSMutableDictionary dictionary]; } if (!self.messages) { self.messages = [NSMutableArray array]; } self.shouldAutoPlayPrologueAudio = NO; [self.chatView updateIntroFooterText:nil]; [self ensureOpeningMessageAtTop]; KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage]; self.pendingAssistantMessages[requestId] = message; if (self.chatView.inverted) { [self.messages insertObject:message atIndex:0]; } else { [self.messages addObject:message]; } [self.chatView addMessage:message autoScroll:YES]; } /// 移除 loading AI 消息 - (void)removeLoadingAssistantMessage { // 从数据源中移除 if (self.chatView.inverted) { for (NSInteger i = 0; i < self.messages.count; i++) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) { [self.messages removeObjectAtIndex:i]; break; } } } else { for (NSInteger i = self.messages.count - 1; i >= 0; i--) { KBAiChatMessage *message = self.messages[i]; if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) { [self.messages removeObjectAtIndex:i]; break; } } } // 从 chatView 中移除 [self.chatView removeLoadingAssistantMessage]; } - (void)removeLoadingAssistantMessageWithRequestId:(NSString *)requestId { if (requestId.length == 0) { [self removeLoadingAssistantMessage]; return; } KBAiChatMessage *target = self.pendingAssistantMessages[requestId]; if (!target) { return; } [self.pendingAssistantMessages removeObjectForKey:requestId]; NSInteger idx = [self.messages indexOfObjectIdenticalTo:target]; if (idx != NSNotFound) { [self.messages removeObjectAtIndex:idx]; [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:NO]; } } - (void)updateAssistantMessageWithRequestId:(NSString *)requestId text:(NSString *)text audioId:(nullable NSString *)audioId { if (requestId.length == 0) { [self appendAssistantMessage:text audioId:audioId]; return; } KBAiChatMessage *target = self.pendingAssistantMessages[requestId]; [self.pendingAssistantMessages removeObjectForKey:requestId]; if (!target) { [self appendAssistantMessage:text audioId:audioId]; return; } target.isLoading = NO; target.text = text ?: @""; target.audioId = audioId; target.needsTypewriterEffect = YES; target.isComplete = NO; [self.chatView reloadMessage:target]; } - (void)updateChatViewBottomInset:(CGFloat)bottomInset { [self.chatView updateContentBottomInset:bottomInset]; } #pragma mark - KBChatTableViewDelegate - (void)chatTableViewDidScroll:(KBChatTableView *)chatView scrollView:(UIScrollView *)scrollView { CGFloat offsetY = scrollView.contentOffset.y; if (chatView.inverted) { CGFloat contentHeight = scrollView.contentSize.height; CGFloat scrollViewHeight = scrollView.bounds.size.height; CGFloat maxOffsetY = contentHeight - scrollViewHeight + scrollView.contentInset.bottom; if (maxOffsetY < 0) { maxOffsetY = 0; } if (offsetY >= maxOffsetY - 50 && !self.isLoading && self.canTriggerLoadMore && self.hasMoreHistory) { self.canTriggerLoadMore = NO; [self loadMoreHistory]; } else if (offsetY < maxOffsetY - 100) { self.canTriggerLoadMore = YES; } return; } if (offsetY <= 50 && !self.isLoading && self.canTriggerLoadMore && self.hasMoreHistory) { self.canTriggerLoadMore = NO; [self loadMoreHistory]; } else if (offsetY > -20) { self.canTriggerLoadMore = YES; } } - (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView { [self loadMoreHistory]; } #pragma mark - Lazy Load - (UIImageView *)backgroundImageView { if (!_backgroundImageView) { _backgroundImageView = [[UIImageView alloc] init]; _backgroundImageView.contentMode = UIViewContentModeScaleAspectFill; _backgroundImageView.clipsToBounds = YES; } return _backgroundImageView; } - (UIImageView *)avatarImageView { if (!_avatarImageView) { _avatarImageView = [[UIImageView alloc] init]; _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; _avatarImageView.layer.cornerRadius = 27; _avatarImageView.layer.borderWidth = 3; _avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor; _avatarImageView.clipsToBounds = YES; _avatarImageView.userInteractionEnabled = YES; // 添加点击手势 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarTapped)]; [_avatarImageView addGestureRecognizer:tap]; } return _avatarImageView; } - (UILabel *)nameLabel { if (!_nameLabel) { _nameLabel = [[UILabel alloc] init]; _nameLabel.font = [UIFont boldSystemFontOfSize:12]; _nameLabel.textColor = [UIColor whiteColor]; _nameLabel.textAlignment = NSTextAlignmentCenter; } return _nameLabel; } - (UILabel *)openingLabel { if (!_openingLabel) { _openingLabel = [[UILabel alloc] init]; _openingLabel.font = [UIFont systemFontOfSize:14]; _openingLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; _openingLabel.textAlignment = NSTextAlignmentCenter; _openingLabel.numberOfLines = 2; } return _openingLabel; } - (KBChatTableView *)chatView { if (!_chatView) { _chatView = [[KBChatTableView alloc] init]; _chatView.backgroundColor = [UIColor clearColor]; _chatView.inverted = YES; _chatView.delegate = self; } return _chatView; } - (KBImagePositionButton *)commentButton { if (!_commentButton) { // 创建上图下文的按钮 _commentButton = [[KBImagePositionButton alloc] initWithImagePosition:KBImagePositionTop spacing:4]; // 关键修复:先设置字体,再设置文字,避免循环调用 _commentButton.titleLabel.font = [UIFont systemFontOfSize:10]; // 设置图片 [_commentButton setImage:[UIImage imageNamed:@"ai_comment_icon"] forState:UIControlStateNormal]; // 设置文字 [_commentButton setTitle:@"0" forState:UIControlStateNormal]; [_commentButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.8] forState:UIControlStateNormal]; // 添加点击事件 [_commentButton addTarget:self action:@selector(commentButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; } return _commentButton; } - (KBImagePositionButton *)likeButton { if (!_likeButton) { // 创建上图下文的按钮 _likeButton = [[KBImagePositionButton alloc] initWithImagePosition:KBImagePositionTop spacing:4]; // 关键修复:先设置字体,再设置文字,避免循环调用 _likeButton.titleLabel.font = [UIFont systemFontOfSize:10]; // 设置图片 [_likeButton setImage:[UIImage imageNamed:@"ai_live_icon"] forState:UIControlStateNormal]; [_likeButton setImage:[UIImage imageNamed:@"ai_livesel_icon"] forState:UIControlStateSelected]; // 设置文字 [_likeButton setTitle:@"0" forState:UIControlStateNormal]; [_likeButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.8] forState:UIControlStateNormal]; // 添加点击事件 [_likeButton addTarget:self action:@selector(likeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; } return _likeButton; } #pragma mark - Button Actions - (void)avatarTapped { NSLog(@"[KBPersonaChatCell] 头像点击,跳转到人设详情页"); if (self.persona.personaId <= 0) { NSLog(@"[KBPersonaChatCell] personaId 无效,取消跳转"); return; } AIPersonInfoVC *vc = [[AIPersonInfoVC alloc] init]; vc.companionId = self.persona.personaId; [KB_CURRENT_NAV pushViewController:vc animated:YES]; } - (void)commentButtonTapped:(KBImagePositionButton *)sender { NSLog(@"[KBPersonaChatCell] 评论按钮点击"); // 弹出评论视图 [self showComment]; } - (void)likeButtonTapped:(KBImagePositionButton *)sender { NSLog(@"[KBPersonaChatCell] 喜欢按钮点击"); NSInteger personaId = self.persona.personaId; // 禁用按钮,防止重复点击 sender.enabled = NO; __weak typeof(self) weakSelf = self; [self.aiVM likeCompanionWithCompanionId:personaId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_async(dispatch_get_main_queue(), ^{ // 恢复按钮可用状态 sender.enabled = YES; if (error) { NSLog(@"[KBPersonaChatCell] 点赞失败:%@", error.localizedDescription); // TODO: 显示错误提示 return; } if (response && response.code == 0) { // 获取当前喜欢数 NSInteger currentLikeCount = [strongSelf.persona.likeCount integerValue]; // response.data: true 表示点赞成功,false 表示取消点赞成功 if (response.data) { // 点赞成功,喜欢数加1 currentLikeCount += 1; sender.selected = YES; NSLog(@"[KBPersonaChatCell] 点赞成功,新喜欢数:%ld", (long)currentLikeCount); } else { // 取消点赞成功,喜欢数减1(但不能小于0) currentLikeCount = MAX(0, currentLikeCount - 1); sender.selected = NO; NSLog(@"[KBPersonaChatCell] 取消点赞成功,新喜欢数:%ld", (long)currentLikeCount); } // 更新模型数据 strongSelf.persona.likeCount = [NSString stringWithFormat:@"%ld", (long)currentLikeCount]; strongSelf.persona.liked = sender.selected; // 更新按钮显示文字 [sender setTitle:strongSelf.persona.likeCount forState:UIControlStateNormal]; } else { NSLog(@"[KBPersonaChatCell] 点赞失败:%@", response.message ?: @"未知错误"); // TODO: 显示错误提示 } }); }]; } #pragma mark - Comment View - (void)showComment { // 关闭之前的弹窗 if (self.popView) { [self.popView dismiss]; } CGFloat customViewHeight = KB_SCREEN_HEIGHT * 0.7; KBAICommentView *customView = [[KBAICommentView alloc] initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; NSString *commentCount = self.persona.commentCount; NSInteger totalCommentCount = [commentCount integerValue];; customView.totalCommentCount = totalCommentCount; // 设置评论视图的人设 ID customView.companionId = self.persona.personaId; // 加载评论数据 [customView loadComments]; LSTPopView *popView = [LSTPopView initWithCustomView:customView parentView:nil popStyle:LSTPopStyleSmoothFromBottom dismissStyle:LSTDismissStyleSmoothToBottom]; customView.popView = popView; popView.bgColor = [UIColor clearColor]; self.popView = popView; popView.priority = 1000; popView.isAvoidKeyboard = NO; popView.hemStyle = LSTHemStyleBottom; popView.dragStyle = LSTDragStyleY_Positive; popView.dragDistance = customViewHeight * 0.5; popView.sweepStyle = LSTSweepStyleY_Positive; popView.swipeVelocity = 1600; popView.sweepDismissStyle = LSTSweepDismissStyleSmooth; popView.bgClickBlock = ^{ [KB_CURRENT_NAV.view endEditing:true]; }; [popView pop]; } - (AiVM *)aiVM{ if (!_aiVM) { _aiVM = [[AiVM alloc] init]; } return _aiVM; } @end