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

@@ -9,10 +9,10 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
/// 消息发送者类型 /// 消息发送者类型(与后端保持一致)
typedef NS_ENUM(NSInteger, KBChatSender) { typedef NS_ENUM(NSInteger, KBChatSender) {
KBChatSenderUser = 0, // 用户 KBChatSenderUser = 1, // 用户(右侧显示)
KBChatSenderAssistant = 1 // AI 助手 KBChatSenderAssistant = 2 // AI 助手(左侧显示)
}; };
/// 聊天记录模型 /// 聊天记录模型

View File

@@ -86,13 +86,13 @@ POST /chat/history
"records": [ "records": [
{ {
"id": 1, "id": 1,
"sender": 0, // 0-用户1-AI "sender": 1, // 1-用户右侧2-AI左侧
"content": "你好", "content": "你好",
"createdAt": "2026-01-26 10:00:00" "createdAt": "2026-01-26 10:00:00"
}, },
{ {
"id": 2, "id": 2,
"sender": 1, "sender": 2,
"content": "你好!有什么可以帮助你的吗?", "content": "你好!有什么可以帮助你的吗?",
"createdAt": "2026-01-26 10:00:05" "createdAt": "2026-01-26 10:00:05"
} }
@@ -104,6 +104,10 @@ POST /chat/history
} }
``` ```
### sender 字段说明
- **sender = 1**:用户消息(显示在右侧)
- **sender = 2**AI 消息(显示在左侧)
--- ---
## 📝 使用示例 ## 📝 使用示例
@@ -359,11 +363,24 @@ AiVM *aiVM = [[AiVM alloc] init];
```objc ```objc
typedef NS_ENUM(NSInteger, KBChatSender) { typedef NS_ENUM(NSInteger, KBChatSender) {
KBChatSenderUser = 0, // 用户 KBChatSenderUser = 1, // 用户(右侧显示)
KBChatSenderAssistant = 1 // AI 助手 KBChatSenderAssistant = 2 // AI 助手(左侧显示)
}; };
``` ```
### 与 KBAiChatMessage 的映射关系
```objc
// KBChatHistoryModel → KBAiChatMessage
if (historyModel.sender == KBChatSenderUser) {
// sender = 1 → KBAiChatMessageTypeUser右侧
message = [KBAiChatMessage userMessageWithText:historyModel.content];
} else if (historyModel.sender == KBChatSenderAssistant) {
// sender = 2 → KBAiChatMessageTypeAssistant左侧
message = [KBAiChatMessage assistantMessageWithText:historyModel.content];
}
```
--- ---
## ✅ 已完成功能 ## ✅ 已完成功能

View File

@@ -6,14 +6,24 @@
// //
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "KBAiChatMessage.h"
@class KBChatMessage;
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class KBChatTableView;
@protocol KBChatTableViewDelegate <NSObject>
@optional
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
scrollView:(UIScrollView *)scrollView;
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView;
@end
/// 聊天列表视图支持用户消息、AI 消息、时间戳、语音播放) /// 聊天列表视图支持用户消息、AI 消息、时间戳、语音播放)
@interface KBChatTableView : UIView @interface KBChatTableView : UIView
@property (nonatomic, weak) id<KBChatTableViewDelegate> delegate;
/// 添加用户消息 /// 添加用户消息
- (void)addUserMessage:(NSString *)text; - (void)addUserMessage:(NSString *)text;
@@ -41,6 +51,21 @@ NS_ASSUME_NONNULL_BEGIN
/// 停止正在播放的音频 /// 停止正在播放的音频
- (void)stopPlayingAudio; - (void)stopPlayingAudio;
/// 结束加载更多
- (void)endLoadMoreWithHasMoreData:(BOOL)hasMoreData;
/// 重置无更多数据状态
- (void)resetNoMoreData;
/// 添加自定义消息(可用于历史消息或打字机)
- (void)addMessage:(KBAiChatMessage *)message
autoScroll:(BOOL)autoScroll;
/// 用指定消息重载(用于历史消息分页)
- (void)reloadWithMessages:(NSArray<KBAiChatMessage *> *)messages
keepOffset:(BOOL)keepOffset
scrollToBottom:(BOOL)scrollToBottom;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -11,6 +11,7 @@
#import "KBChatAssistantMessageCell.h" #import "KBChatAssistantMessageCell.h"
#import "KBChatTimeCell.h" #import "KBChatTimeCell.h"
#import "AiVM.h" #import "AiVM.h"
#import <MJRefresh/MJRefresh.h>
#import <Masonry/Masonry.h> #import <Masonry/Masonry.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
@@ -28,6 +29,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
@property (nonatomic, strong) AVAudioPlayer *audioPlayer; @property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, strong) NSIndexPath *playingCellIndexPath; @property (nonatomic, strong) NSIndexPath *playingCellIndexPath;
@property (nonatomic, strong) AiVM *aiVM; @property (nonatomic, strong) AiVM *aiVM;
@property (nonatomic, assign) BOOL hasMoreData;
@end @end
@@ -52,6 +54,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
- (void)setup { - (void)setup {
self.messages = [[NSMutableArray alloc] init]; self.messages = [[NSMutableArray alloc] init];
self.aiVM = [[AiVM alloc] init]; self.aiVM = [[AiVM alloc] init];
self.hasMoreData = YES;
// TableView // TableView
self.tableView = [[UITableView alloc] initWithFrame:self.bounds self.tableView = [[UITableView alloc] initWithFrame:self.bounds
@@ -75,126 +78,54 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
// //
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { [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 #pragma mark - Public Methods
- (void)addUserMessage:(NSString *)text { - (void)addUserMessage:(NSString *)text {
//
NSInteger oldCount = self.messages.count;
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
[self insertMessageWithTimestamp:message]; [self addMessage:message autoScroll:YES];
//
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];
});
} }
- (void)addAssistantMessage:(NSString *)text - (void)addAssistantMessage:(NSString *)text
audioDuration:(NSTimeInterval)duration audioDuration:(NSTimeInterval)duration
audioData:(NSData *)audioData { audioData:(NSData *)audioData {
//
NSInteger oldCount = self.messages.count;
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioDuration:duration audioDuration:duration
audioData:audioData]; audioData:audioData];
[self insertMessageWithTimestamp:message]; [self addMessage:message autoScroll:YES];
//
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];
});
} }
- (void)addAssistantMessage:(NSString *)text - (void)addAssistantMessage:(NSString *)text
audioId:(NSString *)audioId { 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 KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioId:audioId]; audioId:audioId];
message.needsTypewriterEffect = YES; // message.needsTypewriterEffect = YES; //
NSLog(@"[KBChatTableView] 新消息属性 - needsTypewriter: %d, isComplete: %d", [self addMessage:message autoScroll:YES];
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] ========== 添加完成 ==========");
} }
- (void)updateLastAssistantMessage:(NSString *)text { - (void)updateLastAssistantMessage:(NSString *)text {
@@ -245,6 +176,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
- (void)clearMessages { - (void)clearMessages {
[self.messages removeAllObjects]; [self.messages removeAllObjects];
[self.tableView reloadData]; [self.tableView reloadData];
[self updateFooterVisibility];
} }
- (void)scrollToBottom { - (void)scrollToBottom {
@@ -257,6 +189,92 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
animated:YES]; 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 #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 #pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - (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 #pragma mark - KBChatAssistantMessageCellDelegate
- (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell - (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell

View File

@@ -19,6 +19,13 @@ NS_ASSUME_NONNULL_BEGIN
/// 预加载数据 /// 预加载数据
- (void)preloadDataIfNeeded; - (void)preloadDataIfNeeded;
/// 添加用户消息
- (void)appendUserMessage:(NSString *)text;
/// 添加 AI 消息(支持打字机效果)
- (void)appendAssistantMessage:(NSString *)text
audioId:(nullable NSString *)audioId;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -13,7 +13,7 @@
#import <Masonry/Masonry.h> #import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h> #import <SDWebImage/SDWebImage.h>
@interface KBPersonaChatCell () <UITableViewDelegate, UITableViewDataSource> @interface KBPersonaChatCell () <KBChatTableViewDelegate>
/// ///
@property (nonatomic, strong) UIImageView *backgroundImageView; @property (nonatomic, strong) UIImageView *backgroundImageView;
@@ -28,7 +28,7 @@
@property (nonatomic, strong) UILabel *openingLabel; @property (nonatomic, strong) UILabel *openingLabel;
/// ///
@property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) KBChatTableView *chatView;
/// ///
@property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages; @property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages;
@@ -102,8 +102,8 @@
}]; }];
// //
[self.contentView addSubview:self.tableView]; [self.contentView addSubview:self.chatView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { [self.chatView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.openingLabel.mas_bottom).offset(30); make.top.equalTo(self.openingLabel.mas_bottom).offset(30);
make.left.right.bottom.equalTo(self.contentView); make.left.right.bottom.equalTo(self.contentView);
}]; }];
@@ -130,7 +130,7 @@
self.nameLabel.text = persona.name; self.nameLabel.text = persona.name;
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText; self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
[self.tableView reloadData]; [self.chatView clearMessages];
} }
#pragma mark - 2 #pragma mark - 2
@@ -145,11 +145,16 @@
- (void)loadChatHistory { - (void)loadChatHistory {
if (self.isLoading || !self.hasMoreHistory) { if (self.isLoading || !self.hasMoreHistory) {
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
return; return;
} }
self.isLoading = YES; self.isLoading = YES;
if (self.currentPage == 1) {
[self.chatView resetNoMoreData];
}
// 使 persona.personaId companionId // 使 persona.personaId companionId
NSInteger companionId = self.persona.personaId; NSInteger companionId = self.persona.personaId;
@@ -162,6 +167,7 @@
if (error) { if (error) {
NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription);
[weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
// //
if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) { if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) {
@@ -177,12 +183,24 @@
NSMutableArray *newMessages = [NSMutableArray array]; NSMutableArray *newMessages = [NSMutableArray array];
for (KBChatHistoryModel *item in pageModel.records) { for (KBChatHistoryModel *item in pageModel.records) {
KBAiChatMessage *message; KBAiChatMessage *message;
if (item.isUserMessage) {
// sender
// sender = 1:
// sender = 2: AI
if (item.sender == KBChatSenderUser) {
//
message = [KBAiChatMessage userMessageWithText:item.content]; message = [KBAiChatMessage userMessageWithText:item.content];
} else if (item.sender == KBChatSenderAssistant) {
// AI
message = [KBAiChatMessage assistantMessageWithText:item.content];
} else { } else {
// AI
NSLog(@"[KBPersonaChatCell] 未知的 sender 类型:%ld", (long)item.sender);
message = [KBAiChatMessage assistantMessageWithText:item.content]; message = [KBAiChatMessage assistantMessageWithText:item.content];
} }
message.isComplete = YES; message.isComplete = YES;
message.needsTypewriterEffect = NO;
[newMessages addObject:message]; [newMessages addObject:message];
} }
@@ -198,24 +216,12 @@
// UI // UI
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.currentPage == 1) { BOOL keepOffset = (weakSelf.currentPage != 1);
[weakSelf.tableView reloadData]; BOOL scrollToBottom = (weakSelf.currentPage == 1);
[weakSelf.chatView reloadWithMessages:weakSelf.messages
// keepOffset:keepOffset
if (weakSelf.messages.count > 0) { scrollToBottom:scrollToBottom];
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:weakSelf.messages.count - 1 inSection:0]; [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
[weakSelf.tableView scrollToRowAtIndexPath:lastIndexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:NO];
}
} else {
//
CGFloat oldContentHeight = weakSelf.tableView.contentSize.height;
[weakSelf.tableView reloadData];
CGFloat newContentHeight = weakSelf.tableView.contentSize.height;
CGFloat offsetY = newContentHeight - oldContentHeight;
[weakSelf.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
}
}); });
NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@",
@@ -227,6 +233,7 @@
- (void)loadMoreHistory { - (void)loadMoreHistory {
if (!self.hasMoreHistory || self.isLoading) { if (!self.hasMoreHistory || self.isLoading) {
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
return; return;
} }
@@ -238,41 +245,53 @@
// //
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText];
openingMsg.isComplete = YES; openingMsg.isComplete = YES;
openingMsg.needsTypewriterEffect = NO;
[self.messages addObject:openingMsg]; [self.messages addObject:openingMsg];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; [self.chatView reloadWithMessages:self.messages
keepOffset:NO
scrollToBottom:YES];
}); });
} }
#pragma mark - UITableViewDataSource #pragma mark - 3
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - (void)appendUserMessage:(NSString *)text {
return self.messages.count; if (text.length == 0) {
return;
} }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (!self.messages) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; self.messages = [NSMutableArray array];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
cell.backgroundColor = [UIColor clearColor];
cell.textLabel.textColor = [UIColor whiteColor];
cell.textLabel.numberOfLines = 0;
} }
KBAiChatMessage *message = self.messages[indexPath.row]; KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
cell.textLabel.text = message.text; [self.messages addObject:message];
[self.chatView addMessage:message autoScroll:YES];
return cell;
} }
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - (void)appendAssistantMessage:(NSString *)text
return UITableViewAutomaticDimension; audioId:(NSString *)audioId {
if (text.length == 0) {
return;
} }
#pragma mark - UIScrollViewDelegate if (!self.messages) {
self.messages = [NSMutableArray array];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioId:audioId];
message.needsTypewriterEffect = YES;
[self.messages addObject:message];
[self.chatView addMessage:message autoScroll:YES];
}
#pragma mark - KBChatTableViewDelegate
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
scrollView:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y; CGFloat offsetY = scrollView.contentOffset.y;
// //
@@ -281,6 +300,10 @@
} }
} }
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView {
[self loadMoreHistory];
}
#pragma mark - Lazy Load #pragma mark - Lazy Load
- (UIImageView *)backgroundImageView { - (UIImageView *)backgroundImageView {
@@ -325,22 +348,13 @@
return _openingLabel; return _openingLabel;
} }
- (UITableView *)tableView { - (KBChatTableView *)chatView {
if (!_tableView) { if (!_chatView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _chatView = [[KBChatTableView alloc] init];
_tableView.delegate = self; _chatView.backgroundColor = [UIColor clearColor];
_tableView.dataSource = self; _chatView.delegate = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.backgroundColor = [UIColor clearColor];
_tableView.showsVerticalScrollIndicator = NO;
_tableView.estimatedRowHeight = 60;
_tableView.rowHeight = UITableViewAutomaticDimension;
if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} }
} return _chatView;
return _tableView;
} }
@end @end

View File

@@ -81,7 +81,7 @@
[self.view addSubview:self.voiceInputBar]; [self.view addSubview:self.voiceInputBar];
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) { [self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view); make.left.right.equalTo(self.view);
make.bottom.equalTo(self.view); make.bottom.equalTo(self.view).offset(-20);
make.height.mas_equalTo(150); // make.height.mas_equalTo(150); //
}]; }];
} }
@@ -267,6 +267,27 @@
return persona.personaId; return persona.personaId;
} }
- (KBPersonaChatCell *)currentPersonaCell {
if (self.personas.count == 0) {
return nil;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0];
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
if (cell) {
return cell;
}
for (NSIndexPath *visibleIndex in self.collectionView.indexPathsForVisibleItems) {
KBPersonaChatCell *visibleCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:visibleIndex];
if (visibleCell) {
return visibleCell;
}
}
return nil;
}
#pragma mark - Lazy Load #pragma mark - Lazy Load
- (UICollectionView *)collectionView { - (UICollectionView *)collectionView {
@@ -315,14 +336,43 @@
return; return;
} }
KBPersonaChatCell *currentCell = [self currentPersonaCell];
if (currentCell) {
[currentCell appendUserMessage:text];
}
__weak typeof(self) weakSelf = self;
[self.aiVM requestChatMessageWithContent:text [self.aiVM requestChatMessageWithContent:text
companionId:companionId companionId:companionId
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) { completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (error) { if (error) {
NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
return; return;
} }
NSLog(@"[KBAIHomeVC] 聊天请求成功code=%ld", (long)response.code);
if (!response || !response.data) {
NSLog(@"[KBAIHomeVC] 聊天响应为空");
return;
}
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
NSString *audioId = response.data.audioId;
if (aiResponse.length == 0) {
NSLog(@"[KBAIHomeVC] AI 回复为空");
return;
}
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
if (cell) {
[cell appendAssistantMessage:aiResponse audioId:audioId];
}
});
}]; }];
} }