Files
keyboard/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m
2026-02-02 14:29:42 +08:00

1073 lines
37 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 "KBAiChatMessage.h"
#import "KBChatHistoryPageModel.h"
#import "AiVM.h"
#import "KBImagePositionButton.h"
#import "KBAICommentView.h"
#import "KBAIChatMessageCacheManager.h"
#import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h>
#import <LSTPopView/LSTPopView.h>
#import "AIPersonInfoVC.h"
/// 聊天会话被重置的通知
static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification";
@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) NSMutableArray<KBAiChatMessage *> *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<NSString *, KBAiChatMessage *> *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