// // 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) UITableView *tableView; /// 聊天消息 @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.tableView]; [self.tableView 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.tableView reloadData]; } #pragma mark - 2:数据加载 - (void)preloadDataIfNeeded { if (self.hasLoadedData || self.isLoading) { return; } [self loadChatHistory]; } - (void)loadChatHistory { if (self.isLoading || !self.hasMoreHistory) { return; } self.isLoading = YES; // 使用 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); // 如果是第一次加载失败,显示开场白 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; if (item.isUserMessage) { message = [KBAiChatMessage userMessageWithText:item.content]; } else { message = [KBAiChatMessage assistantMessageWithText:item.content]; } message.isComplete = YES; [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(), ^{ 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]; } }); NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", (long)weakSelf.currentPage, (long)newMessages.count, pageModel.hasMore ? @"是" : @"否"); }]; } - (void)loadMoreHistory { if (!self.hasMoreHistory || self.isLoading) { return; } self.currentPage++; [self loadChatHistory]; } - (void)showOpeningMessage { // 显示开场白作为第一条消息 KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; openingMsg.isComplete = YES; [self.messages addObject:openingMsg]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); } #pragma mark - UITableViewDataSource - (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; } KBAiChatMessage *message = self.messages[indexPath.row]; cell.textLabel.text = message.text; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { CGFloat offsetY = scrollView.contentOffset.y; // 下拉到顶部,加载历史消息 if (offsetY <= -50 && !self.isLoading) { [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; } - (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; } } return _tableView; } @end