diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h index 2944651..39f5a6e 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.h @@ -91,6 +91,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)appendHistoryMessages:(NSArray *)messages openingMessage:(nullable KBAiChatMessage *)openingMessage; +/// 刷新指定消息(按对象指针匹配) +- (void)reloadMessage:(KBAiChatMessage *)message; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m index 3b22a42..87d5676 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBChatTableView.m @@ -868,6 +868,18 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) { }]; } +- (void)reloadMessage:(KBAiChatMessage *)message { + if (!message) { + return; + } + NSInteger index = [self.messages indexOfObjectIdenticalTo:message]; + if (index == NSNotFound) { + return; + } + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; +} + /// 判断是否需要插入时间戳 - (BOOL)shouldInsertTimestampForMessage:(KBAiChatMessage *)message { // 第一条消息总是显示时间 diff --git a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h index eced63d..30e38c4 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h +++ b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.h @@ -24,6 +24,9 @@ NS_ASSUME_NONNULL_BEGIN /// 添加用户消息 - (void)appendUserMessage:(NSString *)text; +/// 添加用户消息(绑定本次请求 requestId) +- (void)appendUserMessage:(NSString *)text requestId:(NSString *)requestId; + /// 标记最后一条用户消息结束加载 - (void)markLastUserMessageLoadingComplete; @@ -40,12 +43,23 @@ NS_ASSUME_NONNULL_BEGIN /// 添加 loading AI 消息 - (void)appendLoadingAssistantMessage; +/// 添加 loading AI 消息(绑定本次请求 requestId) +- (void)appendLoadingAssistantMessageWithRequestId:(NSString *)requestId; + /// 移除 loading AI 消息 - (void)removeLoadingAssistantMessage; +/// 移除 loading AI 消息(按 requestId 精确移除) +- (void)removeLoadingAssistantMessageWithRequestId:(NSString *)requestId; + /// 更新聊天列表底部 inset - (void)updateChatViewBottomInset:(CGFloat)bottomInset; +/// 更新指定 requestId 对应的 AI 回复(替换 loading 占位消息) +- (void)updateAssistantMessageWithRequestId:(NSString *)requestId + text:(NSString *)text + audioId:(nullable NSString *)audioId; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m index 69091e7..32fd9f9 100644 --- a/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/Chat/KBPersonaChatCell.m @@ -64,6 +64,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe /// 评论弹窗 @property (nonatomic, weak) LSTPopView *popView; +@property (nonatomic, strong) NSMutableDictionary *pendingAssistantMessages; + @end @implementation KBPersonaChatCell @@ -97,6 +99,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe // 重置加载状态标志(但不清空 hasLoadedData) self.isLoading = NO; self.canTriggerLoadMore = YES; + [self.pendingAssistantMessages removeAllObjects]; // ✅ 移除了 self.hasLoadedData = NO; // 这样 Cell 复用时不会重复请求数据 @@ -180,6 +183,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe self.canTriggerLoadMore = YES; self.currentPage = 1; self.hasMoreHistory = YES; + [self.pendingAssistantMessages removeAllObjects]; // ⚠️ 临时禁用缓存,排查问题 // NSArray *cachedMessages = [[KBAIChatMessageCacheManager shared] messagesForCompanionId:persona.personaId]; @@ -506,6 +510,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe [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]; @@ -613,6 +621,33 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe [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 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 { // 从数据源中移除 @@ -638,6 +673,51 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe [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]; } diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index a692cd0..6c54303 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -85,6 +85,8 @@ /// 是否正在等待 AI 回复(用于禁止滚动) @property (nonatomic, assign) BOOL isWaitingForAIResponse; +@property (nonatomic, assign) NSInteger pendingAIRequestCount; + /// 右上角消息按钮 @property (nonatomic, strong) UIButton *messageButton; @@ -136,6 +138,7 @@ self.preloadedIndexes = [NSMutableSet set]; self.aiVM = [[AiVM alloc] init]; self.isWaitingForAIResponse = NO; + self.pendingAIRequestCount = 0; self.isTextInputMode = NO; [self setupUI]; @@ -962,15 +965,18 @@ } KBPersonaChatCell *currentCell = [self currentPersonaCell]; + NSString *requestId = [NSUUID UUID].UUIDString; if (currentCell && appendToUI) { - [currentCell appendUserMessage:text]; - // 添加 loading AI 消息 - [currentCell appendLoadingAssistantMessage]; + [currentCell appendUserMessage:text requestId:requestId]; + [currentCell appendLoadingAssistantMessageWithRequestId:requestId]; } - self.isWaitingForAIResponse = YES; - self.collectionView.scrollEnabled = NO; - NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动"); + self.pendingAIRequestCount += 1; + self.isWaitingForAIResponse = (self.pendingAIRequestCount > 0); + if (self.pendingAIRequestCount == 1) { + self.collectionView.scrollEnabled = NO; + NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动"); + } __weak typeof(self) weakSelf = self; [self.aiVM requestChatMessageWithContent:text @@ -982,19 +988,21 @@ } dispatch_async(dispatch_get_main_queue(), ^{ - strongSelf.isWaitingForAIResponse = NO; - strongSelf.collectionView.scrollEnabled = YES; - NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); + if (strongSelf.pendingAIRequestCount > 0) { + strongSelf.pendingAIRequestCount -= 1; + } + strongSelf.isWaitingForAIResponse = (strongSelf.pendingAIRequestCount > 0); + if (strongSelf.pendingAIRequestCount == 0) { + strongSelf.collectionView.scrollEnabled = YES; + NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); + } KBPersonaChatCell *cell = [strongSelf currentPersonaCell]; - if (cell) { - [cell markLastUserMessageLoadingComplete]; - } if (response.code == 50030) { // 移除 loading 消息 if (cell) { - [cell removeLoadingAssistantMessage]; + [cell removeLoadingAssistantMessageWithRequestId:requestId]; } NSString *message = response.message ?: @""; [strongSelf showChatLimitPopWithMessage:message]; @@ -1004,7 +1012,7 @@ if (!response || !response.data) { // 移除 loading 消息 if (cell) { - [cell removeLoadingAssistantMessage]; + [cell removeLoadingAssistantMessageWithRequestId:requestId]; } NSString *message = response.message ?: @"聊天响应为空"; NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message); @@ -1019,15 +1027,14 @@ if (aiResponse.length == 0) { // 移除 loading 消息 if (cell) { - [cell removeLoadingAssistantMessage]; + [cell removeLoadingAssistantMessageWithRequestId:requestId]; } NSLog(@"[KBAIHomeVC] AI 回复为空"); return; } if (cell) { - // appendAssistantMessage 内部会自动移除 loading 消息 - [cell appendAssistantMessage:aiResponse audioId:audioId]; + [cell updateAssistantMessageWithRequestId:requestId text:aiResponse audioId:audioId]; } }); }];