Files
keyboard/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m
2026-01-26 20:36:51 +08:00

361 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 () <KBChatTableViewDelegate>
/// 背景图
@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<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.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