1
This commit is contained in:
@@ -94,6 +94,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 刷新指定消息(按对象指针匹配)
|
/// 刷新指定消息(按对象指针匹配)
|
||||||
- (void)reloadMessage:(KBAiChatMessage *)message;
|
- (void)reloadMessage:(KBAiChatMessage *)message;
|
||||||
|
|
||||||
|
/// 设置介绍视图文案(使用 tableFooterView 展示;nil/空串表示不显示)
|
||||||
|
- (void)updateIntroFooterText:(nullable NSString *)text;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
@property (nonatomic, strong) AiVM *aiVM;
|
@property (nonatomic, strong) AiVM *aiVM;
|
||||||
@property (nonatomic, assign) BOOL hasMoreData;
|
@property (nonatomic, assign) BOOL hasMoreData;
|
||||||
@property (nonatomic, assign) CGFloat contentBottomInset;
|
@property (nonatomic, assign) CGFloat contentBottomInset;
|
||||||
|
@property (nonatomic, copy) NSString *introFooterText;
|
||||||
|
@property (nonatomic, strong) UIView *introFooterContainer;
|
||||||
|
@property (nonatomic, strong) UILabel *introFooterLabel;
|
||||||
|
@property (nonatomic, assign) CGSize lastIntroFooterTableSize;
|
||||||
|
@property (nonatomic, assign) BOOL applyingIntroFooter;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -135,6 +140,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
|
|
||||||
self.tableView.transform = inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
self.tableView.transform = inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||||
[self updateContentBottomInset:self.contentBottomInset];
|
[self updateContentBottomInset:self.contentBottomInset];
|
||||||
|
[self updateIntroFooterText:self.introFooterText];
|
||||||
[self.tableView reloadData];
|
[self.tableView reloadData];
|
||||||
[self.tableView layoutIfNeeded];
|
[self.tableView layoutIfNeeded];
|
||||||
}
|
}
|
||||||
@@ -880,6 +886,85 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
|
|||||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)updateIntroFooterText:(nullable NSString *)text {
|
||||||
|
if (self.applyingIntroFooter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.applyingIntroFooter = YES;
|
||||||
|
self.introFooterText = text ?: @"";
|
||||||
|
if (self.introFooterText.length == 0) {
|
||||||
|
self.tableView.tableFooterView = nil;
|
||||||
|
self.lastIntroFooterTableSize = CGSizeZero;
|
||||||
|
self.applyingIntroFooter = NO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.introFooterContainer) {
|
||||||
|
self.introFooterContainer = [[UIView alloc] initWithFrame:CGRectZero];
|
||||||
|
self.introFooterContainer.backgroundColor = [UIColor clearColor];
|
||||||
|
|
||||||
|
self.introFooterLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||||
|
self.introFooterLabel.numberOfLines = 0;
|
||||||
|
self.introFooterLabel.font = [UIFont systemFontOfSize:14];
|
||||||
|
self.introFooterLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
|
||||||
|
self.introFooterLabel.textAlignment = NSTextAlignmentCenter;
|
||||||
|
[self.introFooterContainer addSubview:self.introFooterLabel];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.introFooterLabel.text = self.introFooterText;
|
||||||
|
self.introFooterContainer.transform = self.inverted ? CGAffineTransformMakeScale(1, -1) : CGAffineTransformIdentity;
|
||||||
|
|
||||||
|
CGFloat width = CGRectGetWidth(self.tableView.bounds);
|
||||||
|
if (width <= 0) {
|
||||||
|
width = CGRectGetWidth(self.bounds);
|
||||||
|
}
|
||||||
|
CGFloat height = CGRectGetHeight(self.tableView.bounds);
|
||||||
|
if (height <= 0) {
|
||||||
|
height = CGRectGetHeight(self.bounds);
|
||||||
|
}
|
||||||
|
self.lastIntroFooterTableSize = CGSizeMake(width, height);
|
||||||
|
|
||||||
|
CGFloat horizontalPadding = 24;
|
||||||
|
CGFloat verticalPadding = 16;
|
||||||
|
CGFloat labelWidth = MAX(0, width - horizontalPadding * 2);
|
||||||
|
CGSize labelSize = [self.introFooterLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)];
|
||||||
|
CGFloat containerHeight = MAX(height, labelSize.height + verticalPadding * 2);
|
||||||
|
|
||||||
|
self.introFooterContainer.frame = CGRectMake(0, 0, width, containerHeight);
|
||||||
|
self.introFooterLabel.frame = CGRectMake(horizontalPadding, verticalPadding, labelWidth, labelSize.height);
|
||||||
|
self.tableView.tableFooterView = self.introFooterContainer;
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self.tableView layoutIfNeeded];
|
||||||
|
CGFloat contentHeight = self.tableView.contentSize.height;
|
||||||
|
CGFloat tableHeight = CGRectGetHeight(self.tableView.bounds);
|
||||||
|
CGFloat minOffset = -self.tableView.contentInset.top;
|
||||||
|
CGFloat maxOffset = contentHeight - tableHeight + self.tableView.contentInset.bottom;
|
||||||
|
if (maxOffset < minOffset) {
|
||||||
|
maxOffset = minOffset;
|
||||||
|
}
|
||||||
|
CGFloat targetOffset = self.inverted ? maxOffset : minOffset;
|
||||||
|
[self.tableView setContentOffset:CGPointMake(0, targetOffset) animated:NO];
|
||||||
|
});
|
||||||
|
self.applyingIntroFooter = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)layoutSubviews {
|
||||||
|
[super layoutSubviews];
|
||||||
|
if (self.introFooterText.length == 0 || self.applyingIntroFooter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGFloat width = CGRectGetWidth(self.tableView.bounds);
|
||||||
|
CGFloat height = CGRectGetHeight(self.tableView.bounds);
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGSize size = CGSizeMake(width, height);
|
||||||
|
if (!CGSizeEqualToSize(size, self.lastIntroFooterTableSize)) {
|
||||||
|
[self updateIntroFooterText:self.introFooterText];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 判断是否需要插入时间戳
|
/// 判断是否需要插入时间戳
|
||||||
- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message {
|
- (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message {
|
||||||
// 第一条消息总是显示时间
|
// 第一条消息总是显示时间
|
||||||
|
|||||||
@@ -175,6 +175,13 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
|
|
||||||
#pragma mark - Setter
|
#pragma mark - Setter
|
||||||
|
|
||||||
|
- (NSString *)currentPrologueText {
|
||||||
|
if (self.persona.prologue.length > 0) {
|
||||||
|
return self.persona.prologue;
|
||||||
|
}
|
||||||
|
return self.persona.introText ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setPersona:(KBPersonaModel *)persona {
|
- (void)setPersona:(KBPersonaModel *)persona {
|
||||||
_persona = persona;
|
_persona = persona;
|
||||||
|
|
||||||
@@ -203,22 +210,20 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
||||||
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
||||||
self.nameLabel.text = persona.name;
|
self.nameLabel.text = persona.name;
|
||||||
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
|
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.prologue;
|
||||||
|
|
||||||
// 关键修复:清空消息时停止音频播放,避免状态混乱
|
// 关键修复:清空消息时停止音频播放,避免状态混乱
|
||||||
[self.chatView stopPlayingAudio];
|
[self.chatView stopPlayingAudio];
|
||||||
|
|
||||||
// 确保开场白在第一条
|
|
||||||
[self ensureOpeningMessageAtTop];
|
|
||||||
|
|
||||||
NSLog(@"[KBPersonaChatCell] ========== setPersona 调试 ==========");
|
NSLog(@"[KBPersonaChatCell] ========== setPersona 调试 ==========");
|
||||||
NSLog(@"[KBPersonaChatCell] personaId: %ld", (long)persona.personaId);
|
NSLog(@"[KBPersonaChatCell] personaId: %ld", (long)persona.personaId);
|
||||||
NSLog(@"[KBPersonaChatCell] messages.count: %ld", (long)self.messages.count);
|
NSLog(@"[KBPersonaChatCell] messages.count: %ld", (long)self.messages.count);
|
||||||
NSLog(@"[KBPersonaChatCell] chatView.frame: %@", NSStringFromCGRect(self.chatView.frame));
|
NSLog(@"[KBPersonaChatCell] chatView.frame: %@", NSStringFromCGRect(self.chatView.frame));
|
||||||
NSLog(@"[KBPersonaChatCell] contentView.frame: %@", NSStringFromCGRect(self.contentView.frame));
|
NSLog(@"[KBPersonaChatCell] contentView.frame: %@", NSStringFromCGRect(self.contentView.frame));
|
||||||
|
|
||||||
// 如果有消息,直接显示(包含开场白)
|
|
||||||
if (self.messages.count > 0) {
|
if (self.messages.count > 0) {
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
|
[self ensureOpeningMessageAtTop];
|
||||||
// 同步缓存,避免下次从缓存缺少开场白
|
// 同步缓存,避免下次从缓存缺少开场白
|
||||||
[[KBAIChatMessageCacheManager shared] saveMessages:self.messages
|
[[KBAIChatMessageCacheManager shared] saveMessages:self.messages
|
||||||
forCompanionId:persona.personaId];
|
forCompanionId:persona.personaId];
|
||||||
@@ -227,6 +232,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
scrollToBottom:YES];
|
scrollToBottom:YES];
|
||||||
} else {
|
} else {
|
||||||
[self.chatView clearMessages];
|
[self.chatView clearMessages];
|
||||||
|
[self.chatView updateIntroFooterText:persona.prologue];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"[KBPersonaChatCell] ========== setPersona 结束 ==========");
|
NSLog(@"[KBPersonaChatCell] ========== setPersona 结束 ==========");
|
||||||
@@ -276,7 +282,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
strongSelf.isLoading = NO;
|
strongSelf.isLoading = NO;
|
||||||
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
|
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
|
||||||
if (strongSelf.currentPage == 1 && strongSelf.persona.introText.length > 0) {
|
if (strongSelf.currentPage == 1 && strongSelf.persona.prologue.length > 0) {
|
||||||
[strongSelf showOpeningMessage];
|
[strongSelf showOpeningMessage];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -286,6 +292,18 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
strongSelf.hasLoadedData = YES;
|
strongSelf.hasLoadedData = YES;
|
||||||
strongSelf.hasMoreHistory = pageModel.hasMore;
|
strongSelf.hasMoreHistory = pageModel.hasMore;
|
||||||
|
|
||||||
|
NSInteger loadedPage = strongSelf.currentPage;
|
||||||
|
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
|
// 转换为 KBAiChatMessage
|
||||||
NSMutableArray *newMessages = [NSMutableArray array];
|
NSMutableArray *newMessages = [NSMutableArray array];
|
||||||
for (KBChatHistoryModel *item in pageModel.records) {
|
for (KBChatHistoryModel *item in pageModel.records) {
|
||||||
@@ -313,8 +331,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 插入历史消息(确保开场白始终是第一条)
|
// 插入历史消息(确保开场白始终是第一条)
|
||||||
// 关键修复:在 dispatch_async 之前保存当前页码,避免异步执行时 currentPage 已经被递增
|
[strongSelf.chatView updateIntroFooterText:nil];
|
||||||
NSInteger loadedPage = strongSelf.currentPage;
|
|
||||||
|
|
||||||
if (loadedPage == 1) {
|
if (loadedPage == 1) {
|
||||||
// 第一页,直接赋值
|
// 第一页,直接赋值
|
||||||
@@ -384,7 +401,13 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)showOpeningMessage {
|
- (void)showOpeningMessage {
|
||||||
// 显示开场白作为第一条消息
|
if (self.messages.count == 0) {
|
||||||
|
[self.chatView clearMessages];
|
||||||
|
[self.chatView updateIntroFooterText:self.persona.prologue];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
@@ -408,16 +431,16 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
if (!message) {
|
if (!message) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
NSString *introText = self.persona.introText ?: @"";
|
NSString *prologue = [self currentPrologueText];
|
||||||
if (introText.length == 0) {
|
if (prologue.length == 0) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return (message.type == KBAiChatMessageTypeAssistant) && [message.text isEqualToString:introText];
|
return (message.type == KBAiChatMessageTypeAssistant) && [message.text isEqualToString:prologue];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)ensureOpeningMessageAtTop {
|
- (void)ensureOpeningMessageAtTop {
|
||||||
NSString *introText = self.persona.introText ?: @"";
|
NSString *prologue = [self currentPrologueText];
|
||||||
if (introText.length == 0) {
|
if (prologue.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!self.messages) {
|
if (!self.messages) {
|
||||||
@@ -426,7 +449,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
if ([self hasOpeningMessageAtTop]) {
|
if ([self hasOpeningMessageAtTop]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:introText];
|
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:prologue];
|
||||||
openingMsg.isComplete = YES;
|
openingMsg.isComplete = YES;
|
||||||
openingMsg.needsTypewriterEffect = NO;
|
openingMsg.needsTypewriterEffect = NO;
|
||||||
if (self.chatView.inverted) {
|
if (self.chatView.inverted) {
|
||||||
@@ -445,8 +468,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger)openingMessageIndexInMessages {
|
- (NSInteger)openingMessageIndexInMessages {
|
||||||
NSString *introText = self.persona.introText ?: @"";
|
NSString *prologue = [self currentPrologueText];
|
||||||
if (introText.length == 0 || self.messages.count == 0) {
|
if (prologue.length == 0 || self.messages.count == 0) {
|
||||||
return NSNotFound;
|
return NSNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +523,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
||||||
if (self.chatView.inverted) {
|
if (self.chatView.inverted) {
|
||||||
@@ -519,6 +543,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
|
||||||
if (self.chatView.inverted) {
|
if (self.chatView.inverted) {
|
||||||
@@ -589,6 +614,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
|
|
||||||
// 查找并移除 loading 消息
|
// 查找并移除 loading 消息
|
||||||
@@ -611,6 +637,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
||||||
if (self.chatView.inverted) {
|
if (self.chatView.inverted) {
|
||||||
@@ -635,6 +662,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
||||||
self.pendingAssistantMessages[requestId] = message;
|
self.pendingAssistantMessages[requestId] = message;
|
||||||
|
|||||||
Reference in New Issue
Block a user