diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h index 94498b9..96bfd33 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h @@ -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 diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m index 47d1b1f..519ef8a 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m @@ -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 (自动预加载,不播放) /// 预加载音频(不自动播放) diff --git a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h index 30e38c4..96b1e37 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h +++ b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h @@ -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 diff --git a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m index 444081d..d523931 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m @@ -66,6 +66,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe @property (nonatomic, strong) NSMutableDictionary *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]; diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index 6c54303..8f0f7b7 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -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,供键盘扩展使用