// // KBPersonaChatCell.m // keyBoard // // Created by Kiro on 2026/1/26. // #import "KBPersonaChatCell.h" #import "KBChatTableView.h" #import "KBAiChatMessage.h" #import "KBChatHistoryPageModel.h" #import "AiVM.h" #import #import @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) KBChatTableView *chatView; /// 聊天消息 @property (nonatomic, strong) NSMutableArray *messages; /// 是否已加载数据 @property (nonatomic, assign) BOOL hasLoadedData; /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; /// 是否还有更多历史消息 @property (nonatomic, assign) BOOL hasMoreHistory; /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; @end @implementation KBPersonaChatCell #pragma mark - Lifecycle - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupUI]; } return self; } #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.avatarImageView]; [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(80); make.centerX.equalTo(self.contentView); make.size.mas_equalTo(CGSizeMake(80, 80)); }]; // 人设名称 [self.contentView addSubview:self.nameLabel]; [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.avatarImageView.mas_bottom).offset(12); make.centerX.equalTo(self.contentView); }]; // 开场白 [self.contentView addSubview:self.openingLabel]; [self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.nameLabel.mas_bottom).offset(8); make.left.equalTo(self.contentView).offset(40); make.right.equalTo(self.contentView).offset(-40); }]; // 聊天列表 [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); }]; } #pragma mark - Setter - (void)setPersona:(KBPersonaModel *)persona { _persona = persona; // 重置状态 self.hasLoadedData = NO; self.isLoading = NO; self.currentPage = 1; self.hasMoreHistory = YES; self.messages = [NSMutableArray array]; self.aiVM = [[AiVM alloc] init]; // 设置 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.introText; [self.chatView clearMessages]; } #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:20 completion:^(KBChatHistoryPageModel *pageModel, NSError *error) { weakSelf.isLoading = NO; if (error) { NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; // 如果是第一次加载失败,显示开场白 if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) { [weakSelf showOpeningMessage]; } return; } weakSelf.hasLoadedData = YES; weakSelf.hasMoreHistory = pageModel.hasMore; // 转换为 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]; } // 插入到顶部(历史消息) if (weakSelf.currentPage == 1) { // 第一页,直接赋值 weakSelf.messages = newMessages; } else { // 后续页,插入到顶部 NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)]; [weakSelf.messages insertObjects:newMessages atIndexes:indexSet]; } // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ 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 条消息,还有更多:%@", (long)weakSelf.currentPage, (long)newMessages.count, pageModel.hasMore ? @"是" : @"否"); }]; } - (void)loadMoreHistory { if (!self.hasMoreHistory || self.isLoading) { [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } self.currentPage++; [self loadChatHistory]; } - (void)showOpeningMessage { // 显示开场白作为第一条消息 KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; openingMsg.isComplete = YES; openingMsg.needsTypewriterEffect = NO; [self.messages addObject:openingMsg]; dispatch_async(dispatch_get_main_queue(), ^{ [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:YES]; }); } #pragma mark - 3:消息追加 - (void)appendUserMessage:(NSString *)text { if (text.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; [self.messages addObject:message]; [self.chatView addMessage:message autoScroll:YES]; } - (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 - KBChatTableViewDelegate - (void)chatTableViewDidScroll:(KBChatTableView *)chatView scrollView:(UIScrollView *)scrollView { CGFloat offsetY = scrollView.contentOffset.y; // 下拉到顶部,加载历史消息 if (offsetY <= -50 && !self.isLoading) { [self loadMoreHistory]; } } - (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 = 40; _avatarImageView.layer.borderWidth = 3; _avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor; _avatarImageView.clipsToBounds = YES; } return _avatarImageView; } - (UILabel *)nameLabel { if (!_nameLabel) { _nameLabel = [[UILabel alloc] init]; _nameLabel.font = [UIFont boldSystemFontOfSize:20]; _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.delegate = self; } return _chatView; } @end