Files
keyboard/CustomKeyboard/View/KBChatPanelView.m
2026-01-30 13:17:11 +08:00

356 lines
15 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBChatPanelView.m
// CustomKeyboard
//
#import "KBChatPanelView.h"
#import "KBChatMessage.h"
#import "KBChatUserCell.h"
#import "KBChatAssistantCell.h"
#import "Masonry.h"
static NSString * const kUserCellIdentifier = @"KBChatUserCell";
static NSString * const kAssistantCellIdentifier = @"KBChatAssistantCell";
static const NSUInteger kKBChatMessageLimit = 10;
@interface KBChatPanelView () <UITableViewDataSource, UITableViewDelegate, KBChatAssistantCellDelegate>
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *closeButton;
@property (nonatomic, strong) UITableView *tableViewInternal;
@property (nonatomic, strong) NSMutableArray<KBChatMessage *> *messages;
@end
@implementation KBChatPanelView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
NSLog(@"[KBChatPanelView] ⚠️ initWithFrame 被调用self=%p", self);
self.backgroundColor = [UIColor clearColor];
self.messages = [NSMutableArray array];
[self addSubview:self.headerView];
[self addSubview:self.tableViewInternal];
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.top.equalTo(self.mas_top);
make.height.mas_equalTo(KBFit(36.0f));
}];
[self.tableViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.top.equalTo(self.headerView.mas_bottom).offset(4);
make.bottom.equalTo(self.mas_bottom).offset(-8);
}];
}
return self;
}
#pragma mark - Public
- (void)kb_reloadWithMessages:(NSArray<KBChatMessage *> *)messages {
NSLog(@"[KBChatPanelView] ========== kb_reloadWithMessages ==========");
NSLog(@"[KBChatPanelView] self=%p, 传入消息数量: %lu", self, (unsigned long)messages.count);
NSLog(@"[KBChatPanelView] 调用堆栈: %@", [NSThread callStackSymbols]);
[self.messages removeAllObjects];
if (messages.count > 0) {
[self.messages addObjectsFromArray:messages];
}
[self.tableViewInternal reloadData];
[self kb_scrollToBottom];
}
- (void)kb_addUserMessage:(NSString *)text {
if (text.length == 0) return;
NSLog(@"[KBChatPanelView] ========== kb_addUserMessage ==========");
NSLog(@"[KBChatPanelView] self=%p, messages=%p", self, self.messages);
NSLog(@"[KBChatPanelView] 添加用户消息: %@", text);
NSLog(@"[KBChatPanelView] 当前消息数量: %lu", (unsigned long)self.messages.count);
KBChatMessage *msg = [KBChatMessage userMessageWithText:text];
NSLog(@"[KBChatPanelView] 创建消息 - outgoing: %d, text: %@", msg.outgoing, msg.text);
[self kb_appendMessage:msg];
NSLog(@"[KBChatPanelView] 添加后消息数量: %lu", (unsigned long)self.messages.count);
for (NSInteger i = 0; i < self.messages.count; i++) {
KBChatMessage *m = self.messages[i];
NSLog(@"[KBChatPanelView] 消息[%ld]: outgoing=%d, isLoading=%d, text=%@", (long)i, m.outgoing, m.isLoading, m.text);
}
}
- (void)kb_addLoadingAssistantMessage {
NSLog(@"[KBChatPanelView] ========== kb_addLoadingAssistantMessage ==========");
NSLog(@"[KBChatPanelView] self=%p, messages=%p", self, self.messages);
NSLog(@"[KBChatPanelView] 当前消息数量: %lu", (unsigned long)self.messages.count);
KBChatMessage *msg = [KBChatMessage loadingAssistantMessage];
NSLog(@"[KBChatPanelView] 创建 loading 消息 - outgoing: %d, isLoading: %d", msg.outgoing, msg.isLoading);
[self kb_appendMessage:msg];
NSLog(@"[KBChatPanelView] 添加后消息数量: %lu", (unsigned long)self.messages.count);
for (NSInteger i = 0; i < self.messages.count; i++) {
KBChatMessage *m = self.messages[i];
NSLog(@"[KBChatPanelView] 消息[%ld]: outgoing=%d, isLoading=%d, text=%@", (long)i, m.outgoing, m.isLoading, m.text);
}
}
- (void)kb_removeLoadingAssistantMessage {
NSLog(@"[KBChatPanelView] ========== kb_removeLoadingAssistantMessage ==========");
NSLog(@"[KBChatPanelView] 当前消息数量: %lu", (unsigned long)self.messages.count);
for (NSInteger i = 0; i < self.messages.count; i++) {
KBChatMessage *m = self.messages[i];
NSLog(@"[KBChatPanelView] 消息[%ld]: outgoing=%d, isLoading=%d, text=%@", (long)i, m.outgoing, m.isLoading, m.text);
}
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBChatMessage *msg = self.messages[i];
NSLog(@"[KBChatPanelView] 检查消息[%ld]: outgoing=%d, isLoading=%d", (long)i, msg.outgoing, msg.isLoading);
// 只移除 AI 消息outgoing == NO且是 loading 状态的
if (!msg.outgoing && msg.isLoading) {
NSLog(@"[KBChatPanelView] ✅ 找到 loading AI 消息,准备移除索引: %ld", (long)i);
[self.messages removeObjectAtIndex:i];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableViewInternal deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationNone];
NSLog(@"[KBChatPanelView] 移除后消息数量: %lu", (unsigned long)self.messages.count);
break;
}
}
NSLog(@"[KBChatPanelView] 最终消息数量: %lu", (unsigned long)self.messages.count);
for (NSInteger i = 0; i < self.messages.count; i++) {
KBChatMessage *m = self.messages[i];
NSLog(@"[KBChatPanelView] 最终消息[%ld]: outgoing=%d, isLoading=%d, text=%@", (long)i, m.outgoing, m.isLoading, m.text);
}
}
- (void)kb_addAssistantMessage:(NSString *)text audioId:(NSString *)audioId {
NSLog(@"[KBChatPanelView] ========== kb_addAssistantMessage ==========");
NSLog(@"[KBChatPanelView] self=%p, messages=%p", self, self.messages);
NSLog(@"[KBChatPanelView] AI 回复文本: %@", text);
NSLog(@"[KBChatPanelView] audioId: %@", audioId);
NSLog(@"[KBChatPanelView] 当前消息数量: %lu", (unsigned long)self.messages.count);
// 先移除 loading 消息
[self kb_removeLoadingAssistantMessage];
NSLog(@"[KBChatPanelView] 移除 loading 后消息数量: %lu", (unsigned long)self.messages.count);
KBChatMessage *msg = [KBChatMessage assistantMessageWithText:text audioId:audioId];
msg.displayName = KBLocalized(@"AI助手");
NSLog(@"[KBChatPanelView] 创建 AI 消息 - outgoing: %d, isLoading: %d, needsTypewriter: %d, text: %@",
msg.outgoing, msg.isLoading, msg.needsTypewriterEffect, msg.text);
[self kb_appendMessage:msg];
NSLog(@"[KBChatPanelView] 添加后消息数量: %lu", (unsigned long)self.messages.count);
for (NSInteger i = 0; i < self.messages.count; i++) {
KBChatMessage *m = self.messages[i];
NSLog(@"[KBChatPanelView] 消息[%ld]: outgoing=%d, isLoading=%d, text=%@", (long)i, m.outgoing, m.isLoading, m.text);
}
}
- (void)kb_updateLastAssistantMessageWithAudioData:(NSData *)audioData duration:(NSTimeInterval)duration {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBChatMessage *msg = self.messages[i];
// 只更新 AI 消息outgoing == NO且非 loading 状态的
if (!msg.outgoing && !msg.isLoading) {
msg.audioData = audioData;
msg.audioDuration = duration;
// 刷新该行以更新语音时长显示
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
KBChatAssistantCell *cell = [self.tableViewInternal cellForRowAtIndexPath:indexPath];
if ([cell isKindOfClass:[KBChatAssistantCell class]]) {
// 直接更新 Cell不刷新整行避免打断打字机效果
if (duration > 0) {
// 通过重新配置来更新时长显示
// 但不要触发打字机效果
msg.needsTypewriterEffect = NO;
msg.isComplete = YES;
}
}
NSLog(@"[KBChatPanelView] 更新 AI 消息音频数据,时长: %.2f秒", duration);
break;
}
}
}
- (void)kb_scrollToBottom {
if (self.messages.count == 0) return;
[self.tableViewInternal layoutIfNeeded];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.messages.count - 1 inSection:0];
[self.tableViewInternal scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
#pragma mark - Private
- (void)kb_appendMessage:(KBChatMessage *)message {
if (!message) return;
NSInteger oldCount = self.messages.count;
[self.messages addObject:message];
// 限制消息数量
if (self.messages.count > kKBChatMessageLimit) {
NSUInteger overflow = self.messages.count - kKBChatMessageLimit;
[self.messages removeObjectsInRange:NSMakeRange(0, overflow)];
[self.tableViewInternal reloadData];
} else {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:oldCount inSection:0];
[self.tableViewInternal insertRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self kb_scrollToBottom];
});
}
#pragma mark - Actions
- (void)kb_onTapClose {
if ([self.delegate respondsToSelector:@selector(chatPanelViewDidTapClose:)]) {
[self.delegate chatPanelViewDidTapClose:self];
}
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.messages.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"[KBChatPanelView] ========== cellForRowAtIndexPath: %ld ==========", (long)indexPath.row);
if (indexPath.row >= self.messages.count) {
NSLog(@"[KBChatPanelView] ❌ 索引越界,返回空 Cell");
return [[UITableViewCell alloc] init];
}
KBChatMessage *msg = self.messages[indexPath.row];
NSLog(@"[KBChatPanelView] 消息: outgoing=%d, isLoading=%d, needsTypewriter=%d, text=%@",
msg.outgoing, msg.isLoading, msg.needsTypewriterEffect, msg.text);
if (msg.outgoing) {
// 用户消息(右侧)
NSLog(@"[KBChatPanelView] 使用 KBChatUserCell");
KBChatUserCell *cell = [tableView dequeueReusableCellWithIdentifier:kUserCellIdentifier forIndexPath:indexPath];
[cell configureWithMessage:msg];
return cell;
} else {
// AI 消息(左侧)
NSLog(@"[KBChatPanelView] 使用 KBChatAssistantCell");
KBChatAssistantCell *cell = [tableView dequeueReusableCellWithIdentifier:kAssistantCellIdentifier forIndexPath:indexPath];
cell.delegate = self;
[cell configureWithMessage:msg];
return cell;
}
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60.0;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= self.messages.count) { return; }
KBChatMessage *msg = self.messages[indexPath.row];
if ([self.delegate respondsToSelector:@selector(chatPanelView:didTapMessage:)]) {
[self.delegate chatPanelView:self didTapMessage:msg];
}
}
#pragma mark - KBChatAssistantCellDelegate
- (void)assistantCell:(KBChatAssistantCell *)cell didTapVoiceButtonForMessage:(KBChatMessage *)message {
if ([self.delegate respondsToSelector:@selector(chatPanelView:didTapVoiceButtonForMessage:)]) {
[self.delegate chatPanelView:self didTapVoiceButtonForMessage:message];
}
}
#pragma mark - Lazy
- (UITableView *)tableViewInternal {
if (!_tableViewInternal) {
_tableViewInternal = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableViewInternal.backgroundColor = [UIColor clearColor];
_tableViewInternal.backgroundView = nil;
_tableViewInternal.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableViewInternal.dataSource = self;
_tableViewInternal.delegate = self;
_tableViewInternal.estimatedRowHeight = 60.0;
_tableViewInternal.rowHeight = UITableViewAutomaticDimension;
// 注册两种 Cell
[_tableViewInternal registerClass:KBChatUserCell.class forCellReuseIdentifier:kUserCellIdentifier];
[_tableViewInternal registerClass:KBChatAssistantCell.class forCellReuseIdentifier:kAssistantCellIdentifier];
if (@available(iOS 11.0, *)) {
_tableViewInternal.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
return _tableViewInternal;
}
- (UIView *)headerView {
if (!_headerView) {
_headerView = [[UIView alloc] init];
_headerView.backgroundColor = [UIColor clearColor];
[_headerView addSubview:self.titleLabel];
[_headerView addSubview:self.closeButton];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_headerView.mas_left).offset(12);
make.centerY.equalTo(_headerView);
}];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(_headerView.mas_right).offset(-12);
make.centerY.equalTo(_headerView);
make.width.height.mas_equalTo(KBFit(24.0f));
}];
}
return _headerView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
_titleLabel.textColor =
[UIColor kb_dynamicColorWithLightColor:[UIColor colorWithHex:0x1B1F1A]
darkColor:[UIColor whiteColor]];
_titleLabel.text = KBLocalized(@"AI对话");
}
return _titleLabel;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *icon = [UIImage imageNamed:@"close_icon"];
[_closeButton setImage:icon forState:UIControlStateNormal];
_closeButton.backgroundColor = [UIColor clearColor];
[_closeButton addTarget:self
action:@selector(kb_onTapClose)
forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
#pragma mark - Expose
- (UITableView *)tableView { return self.tableViewInternal; }
@end