This commit is contained in:
2026-01-26 20:36:51 +08:00
parent 3a5a6395af
commit e8b4b2c58a
7 changed files with 338 additions and 170 deletions

View File

@@ -11,6 +11,7 @@
#import "KBChatAssistantMessageCell.h"
#import "KBChatTimeCell.h"
#import "AiVM.h"
#import <MJRefresh/MJRefresh.h>
#import <Masonry/Masonry.h>
#import <AVFoundation/AVFoundation.h>
@@ -28,6 +29,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, strong) NSIndexPath *playingCellIndexPath;
@property (nonatomic, strong) AiVM *aiVM;
@property (nonatomic, assign) BOOL hasMoreData;
@end
@@ -52,6 +54,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
- (void)setup {
self.messages = [[NSMutableArray alloc] init];
self.aiVM = [[AiVM alloc] init];
self.hasMoreData = YES;
// TableView
self.tableView = [[UITableView alloc] initWithFrame:self.bounds
@@ -75,126 +78,54 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
//
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
// make.edges.equalTo(self);
make.top.left.right.equalTo(self);
make.bottom.equalTo(self).offset(-KB_TABBAR_HEIGHT - 40 - 10);
}];
__weak typeof(self) weakSelf = self;
self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (!strongSelf.hasMoreData) {
[strongSelf.tableView.mj_footer endRefreshingWithNoMoreData];
return;
}
if ([strongSelf.delegate respondsToSelector:@selector(chatTableViewDidTriggerLoadMore:)]) {
[strongSelf.delegate chatTableViewDidTriggerLoadMore:strongSelf];
} else {
[strongSelf.tableView.mj_footer endRefreshing];
}
}];
self.tableView.mj_footer.hidden = YES;
}
#pragma mark - Public Methods
- (void)addUserMessage:(NSString *)text {
//
NSInteger oldCount = self.messages.count;
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
[self insertMessageWithTimestamp:message];
//
NSInteger newCount = self.messages.count;
NSInteger insertedCount = newCount - oldCount;
// 使 insert
if (insertedCount > 0) {
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i = oldCount; i < newCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.tableView insertRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationNone];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self scrollToBottom];
});
[self addMessage:message autoScroll:YES];
}
- (void)addAssistantMessage:(NSString *)text
audioDuration:(NSTimeInterval)duration
audioData:(NSData *)audioData {
//
NSInteger oldCount = self.messages.count;
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioDuration:duration
audioData:audioData];
[self insertMessageWithTimestamp:message];
//
NSInteger newCount = self.messages.count;
NSInteger insertedCount = newCount - oldCount;
// 使 insert
if (insertedCount > 0) {
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i = oldCount; i < newCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.tableView insertRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationNone];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self scrollToBottom];
});
[self addMessage:message autoScroll:YES];
}
- (void)addAssistantMessage:(NSString *)text
audioId:(NSString *)audioId {
NSLog(@"[KBChatTableView] ========== 添加新的 AI 消息 ==========");
NSLog(@"[KBChatTableView] 文本长度: %lu, audioId: %@", (unsigned long)text.length, audioId);
NSLog(@"[KBChatTableView] 当前消息数量: %ld", (long)self.messages.count);
// AI
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *msg = self.messages[i];
if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) {
NSLog(@"[KBChatTableView] 找到上一条未完成的消息 - 索引: %ld, 文本: %@", (long)i, msg.text);
msg.isComplete = YES;
msg.needsTypewriterEffect = NO;
// Cell
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath];
if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) {
NSLog(@"[KBChatTableView] 停止上一条消息的打字机效果");
[oldCell stopTypewriterEffect];
//
oldCell.messageLabel.text = msg.text;
}
break;
}
}
//
NSInteger oldCount = self.messages.count;
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioId:audioId];
message.needsTypewriterEffect = YES; //
NSLog(@"[KBChatTableView] 新消息属性 - needsTypewriter: %d, isComplete: %d",
message.needsTypewriterEffect, message.isComplete);
[self insertMessageWithTimestamp:message];
//
NSInteger newCount = self.messages.count;
NSInteger insertedCount = newCount - oldCount;
NSLog(@"[KBChatTableView] 插入后消息数量: %ld, 新增行数: %ld", (long)newCount, (long)insertedCount);
// 使 insert
if (insertedCount > 0) {
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i = oldCount; i < newCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
NSLog(@"[KBChatTableView] 将插入行: %ld", (long)i);
}
[self.tableView insertRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationNone];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self scrollToBottom];
});
NSLog(@"[KBChatTableView] ========== 添加完成 ==========");
[self addMessage:message autoScroll:YES];
}
- (void)updateLastAssistantMessage:(NSString *)text {
@@ -245,6 +176,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
- (void)clearMessages {
[self.messages removeAllObjects];
[self.tableView reloadData];
[self updateFooterVisibility];
}
- (void)scrollToBottom {
@@ -257,6 +189,92 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
animated:YES];
}
#pragma mark - Public Helpers
- (void)endLoadMoreWithHasMoreData:(BOOL)hasMoreData {
self.hasMoreData = hasMoreData;
if (hasMoreData) {
[self.tableView.mj_footer endRefreshing];
} else {
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
[self updateFooterVisibility];
}
- (void)resetNoMoreData {
self.hasMoreData = YES;
[self.tableView.mj_footer resetNoMoreData];
[self updateFooterVisibility];
}
- (void)addMessage:(KBAiChatMessage *)message
autoScroll:(BOOL)autoScroll {
if (!message) {
return;
}
if (message.type == KBAiChatMessageTypeAssistant &&
message.needsTypewriterEffect &&
!message.isComplete) {
[self stopPreviousIncompleteAssistantMessageIfNeeded];
}
NSInteger oldCount = self.messages.count;
[self insertMessageWithTimestamp:message];
NSInteger newCount = self.messages.count;
NSInteger insertedCount = newCount - oldCount;
if (insertedCount > 0) {
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i = oldCount; i < newCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.tableView insertRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationNone];
}
[self updateFooterVisibility];
if (autoScroll) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self scrollToBottom];
});
}
}
- (void)reloadWithMessages:(NSArray<KBAiChatMessage *> *)messages
keepOffset:(BOOL)keepOffset
scrollToBottom:(BOOL)scrollToBottom {
CGFloat oldContentHeight = self.tableView.contentSize.height;
CGFloat oldOffsetY = self.tableView.contentOffset.y;
[self.messages removeAllObjects];
if (messages.count > 0) {
for (KBAiChatMessage *message in messages) {
[self insertMessageWithTimestamp:message];
}
}
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[self updateFooterVisibility];
if (keepOffset) {
CGFloat newContentHeight = self.tableView.contentSize.height;
CGFloat delta = newContentHeight - oldContentHeight;
CGFloat offsetY = oldOffsetY + delta;
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
return;
}
if (scrollToBottom) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self scrollToBottom];
});
}
}
#pragma mark - Private Methods
///
@@ -323,6 +341,30 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
});
}
- (void)stopPreviousIncompleteAssistantMessageIfNeeded {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *msg = self.messages[i];
if (msg.type == KBAiChatMessageTypeAssistant && !msg.isComplete) {
msg.isComplete = YES;
msg.needsTypewriterEffect = NO;
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
KBChatAssistantMessageCell *oldCell = [self.tableView cellForRowAtIndexPath:oldIndexPath];
if ([oldCell isKindOfClass:[KBChatAssistantMessageCell class]]) {
[oldCell stopTypewriterEffect];
oldCell.messageLabel.text = msg.text;
}
break;
}
}
}
- (void)updateFooterVisibility {
BOOL canLoadMore = (self.delegate &&
[self.delegate respondsToSelector:@selector(chatTableViewDidTriggerLoadMore:)]);
self.tableView.mj_footer.hidden = !canLoadMore || self.messages.count == 0;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
@@ -365,6 +407,19 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
}
}
- (void)setDelegate:(id<KBChatTableViewDelegate>)delegate {
_delegate = delegate;
[self updateFooterVisibility];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ([self.delegate respondsToSelector:@selector(chatTableViewDidScroll:scrollView:)]) {
[self.delegate chatTableViewDidScroll:self scrollView:scrollView];
}
}
#pragma mark - KBChatAssistantMessageCellDelegate
- (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell