1
This commit is contained in:
@@ -97,6 +97,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 设置介绍视图文案(使用 tableFooterView 展示;nil/空串表示不显示)
|
/// 设置介绍视图文案(使用 tableFooterView 展示;nil/空串表示不显示)
|
||||||
- (void)updateIntroFooterText:(nullable NSString *)text;
|
- (void)updateIntroFooterText:(nullable NSString *)text;
|
||||||
|
|
||||||
|
/// 播放远程音频(用于开场白 prologueAudio)
|
||||||
|
- (void)playRemoteAudioWithURLString:(NSString *)urlString;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
@property (nonatomic, strong) UILabel *introFooterLabel;
|
@property (nonatomic, strong) UILabel *introFooterLabel;
|
||||||
@property (nonatomic, assign) CGSize lastIntroFooterTableSize;
|
@property (nonatomic, assign) CGSize lastIntroFooterTableSize;
|
||||||
@property (nonatomic, assign) BOOL applyingIntroFooter;
|
@property (nonatomic, assign) BOOL applyingIntroFooter;
|
||||||
|
@property (nonatomic, copy) NSString *remoteAudioToken;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -894,6 +895,7 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
|
|||||||
self.introFooterText = text ?: @"";
|
self.introFooterText = text ?: @"";
|
||||||
if (self.introFooterText.length == 0) {
|
if (self.introFooterText.length == 0) {
|
||||||
self.tableView.tableFooterView = nil;
|
self.tableView.tableFooterView = nil;
|
||||||
|
self.tableView.scrollEnabled = YES;
|
||||||
self.lastIntroFooterTableSize = CGSizeZero;
|
self.lastIntroFooterTableSize = CGSizeZero;
|
||||||
self.applyingIntroFooter = NO;
|
self.applyingIntroFooter = NO;
|
||||||
return;
|
return;
|
||||||
@@ -907,7 +909,7 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
|
|||||||
self.introFooterLabel.numberOfLines = 0;
|
self.introFooterLabel.numberOfLines = 0;
|
||||||
self.introFooterLabel.font = [UIFont systemFontOfSize:14];
|
self.introFooterLabel.font = [UIFont systemFontOfSize:14];
|
||||||
self.introFooterLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
|
self.introFooterLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
|
||||||
self.introFooterLabel.textAlignment = NSTextAlignmentCenter;
|
self.introFooterLabel.textAlignment = NSTextAlignmentLeft;
|
||||||
[self.introFooterContainer addSubview:self.introFooterLabel];
|
[self.introFooterContainer addSubview:self.introFooterLabel];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,28 +926,25 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
|
|||||||
}
|
}
|
||||||
self.lastIntroFooterTableSize = CGSizeMake(width, height);
|
self.lastIntroFooterTableSize = CGSizeMake(width, height);
|
||||||
|
|
||||||
CGFloat horizontalPadding = 24;
|
CGFloat leftPadding = 16;
|
||||||
CGFloat verticalPadding = 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)];
|
CGSize labelSize = [self.introFooterLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)];
|
||||||
CGFloat containerHeight = MAX(height, labelSize.height + verticalPadding * 2);
|
CGFloat containerHeight = MAX(height, labelSize.height + verticalPadding * 2);
|
||||||
|
|
||||||
self.introFooterContainer.frame = CGRectMake(0, 0, width, containerHeight);
|
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;
|
self.tableView.tableFooterView = self.introFooterContainer;
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.tableView layoutIfNeeded];
|
[self.tableView layoutIfNeeded];
|
||||||
CGFloat contentHeight = self.tableView.contentSize.height;
|
|
||||||
CGFloat tableHeight = CGRectGetHeight(self.tableView.bounds);
|
|
||||||
CGFloat minOffset = -self.tableView.contentInset.top;
|
CGFloat minOffset = -self.tableView.contentInset.top;
|
||||||
CGFloat maxOffset = contentHeight - tableHeight + self.tableView.contentInset.bottom;
|
[self.tableView setContentOffset:CGPointMake(0, minOffset) animated:NO];
|
||||||
if (maxOffset < minOffset) {
|
|
||||||
maxOffset = minOffset;
|
|
||||||
}
|
|
||||||
CGFloat targetOffset = self.inverted ? maxOffset : minOffset;
|
|
||||||
[self.tableView setContentOffset:CGPointMake(0, targetOffset) animated:NO];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.tableView.scrollEnabled = (self.messages.count > 0);
|
||||||
self.applyingIntroFooter = NO;
|
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 (自动预加载,不播放)
|
#pragma mark - Audio Preload (自动预加载,不播放)
|
||||||
|
|
||||||
/// 预加载音频(不自动播放)
|
/// 预加载音频(不自动播放)
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
text:(NSString *)text
|
text:(NSString *)text
|
||||||
audioId:(nullable NSString *)audioId;
|
audioId:(nullable NSString *)audioId;
|
||||||
|
|
||||||
|
/// 当前 Cell 成为屏幕主显示页
|
||||||
|
- (void)onBecameCurrentPersonaCell;
|
||||||
|
|
||||||
|
/// 当前 Cell 不再是屏幕主显示页
|
||||||
|
- (void)onResignedCurrentPersonaCell;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
|
|
||||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, KBAiChatMessage *> *pendingAssistantMessages;
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, KBAiChatMessage *> *pendingAssistantMessages;
|
||||||
|
|
||||||
|
@property (nonatomic, assign) BOOL isCurrentPersonaCell;
|
||||||
|
@property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio;
|
||||||
|
@property (nonatomic, assign) BOOL hasPlayedPrologueAudio;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBPersonaChatCell
|
@implementation KBPersonaChatCell
|
||||||
@@ -100,6 +104,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.isLoading = NO;
|
self.isLoading = NO;
|
||||||
self.canTriggerLoadMore = YES;
|
self.canTriggerLoadMore = YES;
|
||||||
[self.pendingAssistantMessages removeAllObjects];
|
[self.pendingAssistantMessages removeAllObjects];
|
||||||
|
self.isCurrentPersonaCell = NO;
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
|
self.hasPlayedPrologueAudio = NO;
|
||||||
|
|
||||||
// ✅ 移除了 self.hasLoadedData = NO;
|
// ✅ 移除了 self.hasLoadedData = NO;
|
||||||
// 这样 Cell 复用时不会重复请求数据
|
// 这样 Cell 复用时不会重复请求数据
|
||||||
@@ -191,6 +198,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.currentPage = 1;
|
self.currentPage = 1;
|
||||||
self.hasMoreHistory = YES;
|
self.hasMoreHistory = YES;
|
||||||
[self.pendingAssistantMessages removeAllObjects];
|
[self.pendingAssistantMessages removeAllObjects];
|
||||||
|
self.isCurrentPersonaCell = NO;
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
|
self.hasPlayedPrologueAudio = NO;
|
||||||
|
|
||||||
// ⚠️ 临时禁用缓存,排查问题
|
// ⚠️ 临时禁用缓存,排查问题
|
||||||
// NSArray *cachedMessages = [[KBAIChatMessageCacheManager shared] messagesForCompanionId:persona.personaId];
|
// NSArray *cachedMessages = [[KBAIChatMessageCacheManager shared] messagesForCompanionId:persona.personaId];
|
||||||
@@ -293,6 +303,15 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
strongSelf.hasMoreHistory = pageModel.hasMore;
|
strongSelf.hasMoreHistory = pageModel.hasMore;
|
||||||
|
|
||||||
NSInteger loadedPage = strongSelf.currentPage;
|
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) {
|
if (loadedPage == 1 && pageModel.total == 0) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[strongSelf.chatView clearMessages];
|
[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 {
|
- (void)loadMoreHistory {
|
||||||
if (!self.hasMoreHistory || self.isLoading) {
|
if (!self.hasMoreHistory || self.isLoading) {
|
||||||
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
|
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
|
||||||
@@ -523,6 +571,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
|
[self.chatView stopPlayingAudio];
|
||||||
[self.chatView updateIntroFooterText:nil];
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
||||||
@@ -543,6 +593,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
|
[self.chatView stopPlayingAudio];
|
||||||
[self.chatView updateIntroFooterText:nil];
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
|
||||||
@@ -614,6 +666,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
[self.chatView updateIntroFooterText:nil];
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
|
|
||||||
@@ -637,6 +690,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
[self.chatView updateIntroFooterText:nil];
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
||||||
@@ -662,6 +716,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
self.messages = [NSMutableArray array];
|
self.messages = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.shouldAutoPlayPrologueAudio = NO;
|
||||||
[self.chatView updateIntroFooterText:nil];
|
[self.chatView updateIntroFooterText:nil];
|
||||||
[self ensureOpeningMessageAtTop];
|
[self ensureOpeningMessageAtTop];
|
||||||
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
|
||||||
|
|||||||
@@ -151,6 +151,14 @@
|
|||||||
[self loadPersonas];
|
[self loadPersonas];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
|
[super viewDidAppear:animated];
|
||||||
|
KBPersonaChatCell *cell = [self currentPersonaCell];
|
||||||
|
if (cell) {
|
||||||
|
[cell onBecameCurrentPersonaCell];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)viewDidLayoutSubviews {
|
- (void)viewDidLayoutSubviews {
|
||||||
[super viewDidLayoutSubviews];
|
[super viewDidLayoutSubviews];
|
||||||
if (self.bottomMaskLayer) {
|
if (self.bottomMaskLayer) {
|
||||||
@@ -387,8 +395,27 @@
|
|||||||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||||
CGFloat pageHeight = scrollView.bounds.size.height;
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
||||||
NSInteger currentPage = scrollView.contentOffset.y / pageHeight;
|
NSInteger currentPage = scrollView.contentOffset.y / pageHeight;
|
||||||
|
NSInteger previousIndex = self.currentIndex;
|
||||||
self.currentIndex = currentPage;
|
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) {
|
if (currentPage < self.personas.count) {
|
||||||
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
||||||
// 保存当前选中的 persona 到 AppGroup,供键盘扩展使用
|
// 保存当前选中的 persona 到 AppGroup,供键盘扩展使用
|
||||||
|
|||||||
Reference in New Issue
Block a user