This commit is contained in:
2026-02-02 14:29:42 +08:00
parent fe59a0cb45
commit 48c90fa0be
5 changed files with 159 additions and 13 deletions

View File

@@ -97,6 +97,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 设置介绍视图文案(使用 tableFooterView 展示nil/空串表示不显示)
- (void)updateIntroFooterText:(nullable NSString *)text;
/// 播放远程音频(用于开场白 prologueAudio
- (void)playRemoteAudioWithURLString:(NSString *)urlString;
@end
NS_ASSUME_NONNULL_END

View File

@@ -36,6 +36,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
@property (nonatomic, strong) UILabel *introFooterLabel;
@property (nonatomic, assign) CGSize lastIntroFooterTableSize;
@property (nonatomic, assign) BOOL applyingIntroFooter;
@property (nonatomic, copy) NSString *remoteAudioToken;
@end
@@ -894,6 +895,7 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
self.introFooterText = text ?: @"";
if (self.introFooterText.length == 0) {
self.tableView.tableFooterView = nil;
self.tableView.scrollEnabled = YES;
self.lastIntroFooterTableSize = CGSizeZero;
self.applyingIntroFooter = NO;
return;
@@ -907,7 +909,7 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
self.introFooterLabel.numberOfLines = 0;
self.introFooterLabel.font = [UIFont systemFontOfSize:14];
self.introFooterLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
self.introFooterLabel.textAlignment = NSTextAlignmentCenter;
self.introFooterLabel.textAlignment = NSTextAlignmentLeft;
[self.introFooterContainer addSubview:self.introFooterLabel];
}
@@ -924,28 +926,25 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
}
self.lastIntroFooterTableSize = CGSizeMake(width, height);
CGFloat horizontalPadding = 24;
CGFloat leftPadding = 16;
CGFloat verticalPadding = 16;
CGFloat labelWidth = MAX(0, width - horizontalPadding * 2);
CGFloat labelWidth = MAX(0, width * 0.75);
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);
CGFloat labelY = containerHeight - verticalPadding - labelSize.height;
labelY = MAX(verticalPadding, labelY);
self.introFooterLabel.frame = CGRectMake(leftPadding, labelY, 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.tableView setContentOffset:CGPointMake(0, minOffset) animated:NO];
});
self.tableView.scrollEnabled = (self.messages.count > 0);
self.applyingIntroFooter = NO;
}
@@ -1383,6 +1382,62 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
}
}
- (void)playRemoteAudioWithURLString:(NSString *)urlString {
if (urlString.length == 0) {
return;
}
[self stopPlayingAudio];
self.remoteAudioToken = [NSUUID UUID].UUIDString;
NSString *token = self.remoteAudioToken;
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
return;
}
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:^(NSData *_Nullable data,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (![strongSelf.remoteAudioToken isEqualToString:token]) {
return;
}
if (error || !data || data.length == 0) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (![strongSelf.remoteAudioToken isEqualToString:token]) {
return;
}
NSError *sessionError = nil;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
[audioSession setActive:YES error:&sessionError];
NSError *playerError = nil;
strongSelf.audioPlayer = [[AVAudioPlayer alloc] initWithData:data error:&playerError];
if (playerError || !strongSelf.audioPlayer) {
return;
}
strongSelf.audioPlayer.delegate = strongSelf;
strongSelf.audioPlayer.volume = 1.0;
[strongSelf.audioPlayer prepareToPlay];
[strongSelf.audioPlayer play];
});
}];
[task resume];
}
#pragma mark - Audio Preload ()
///

View File

@@ -60,6 +60,12 @@ NS_ASSUME_NONNULL_BEGIN
text:(NSString *)text
audioId:(nullable NSString *)audioId;
/// 当前 Cell 成为屏幕主显示页
- (void)onBecameCurrentPersonaCell;
/// 当前 Cell 不再是屏幕主显示页
- (void)onResignedCurrentPersonaCell;
@end
NS_ASSUME_NONNULL_END

View File

@@ -66,6 +66,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
@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
@@ -100,6 +104,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.isLoading = NO;
self.canTriggerLoadMore = YES;
[self.pendingAssistantMessages removeAllObjects];
self.isCurrentPersonaCell = NO;
self.shouldAutoPlayPrologueAudio = NO;
self.hasPlayedPrologueAudio = NO;
// self.hasLoadedData = NO;
// Cell
@@ -191,6 +198,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
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];
@@ -291,8 +301,17 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
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];
@@ -391,6 +410,35 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
}];
}
#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];
@@ -523,6 +571,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.messages = [NSMutableArray array];
}
self.shouldAutoPlayPrologueAudio = NO;
[self.chatView stopPlayingAudio];
[self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
@@ -543,6 +593,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.messages = [NSMutableArray array];
}
self.shouldAutoPlayPrologueAudio = NO;
[self.chatView stopPlayingAudio];
[self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
@@ -614,6 +666,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.messages = [NSMutableArray array];
}
self.shouldAutoPlayPrologueAudio = NO;
[self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop];
@@ -637,6 +690,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.messages = [NSMutableArray array];
}
self.shouldAutoPlayPrologueAudio = NO;
[self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
@@ -662,6 +716,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.messages = [NSMutableArray array];
}
self.shouldAutoPlayPrologueAudio = NO;
[self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];

View File

@@ -151,6 +151,14 @@
[self loadPersonas];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
KBPersonaChatCell *cell = [self currentPersonaCell];
if (cell) {
[cell onBecameCurrentPersonaCell];
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.bottomMaskLayer) {
@@ -387,8 +395,27 @@
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGFloat pageHeight = scrollView.bounds.size.height;
NSInteger currentPage = scrollView.contentOffset.y / pageHeight;
NSInteger previousIndex = self.currentIndex;
self.currentIndex = currentPage;
if (previousIndex != self.currentIndex) {
NSIndexPath *prevPath = [NSIndexPath indexPathForItem:previousIndex inSection:0];
KBPersonaChatCell *prevCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:prevPath];
if (prevCell) {
[prevCell onResignedCurrentPersonaCell];
}
KBPersonaChatCell *currentCell = [self currentPersonaCell];
if (currentCell) {
[currentCell onBecameCurrentPersonaCell];
}
} else {
KBPersonaChatCell *currentCell = [self currentPersonaCell];
if (currentCell) {
[currentCell onBecameCurrentPersonaCell];
}
}
if (currentPage < self.personas.count) {
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
// persona AppGroup使