Files
keyboard/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m
2026-01-26 18:17:02 +08:00

347 lines
11 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h>
@interface KBPersonaChatCell () <UITableViewDelegate, UITableViewDataSource>
/// 背景图
@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<KBAiChatMessage *> *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