// // 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 () @property (nonatomic, strong) UIView *headerView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *closeButton; @property (nonatomic, strong) UITableView *tableViewInternal; @property (nonatomic, strong) NSMutableArray *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 *)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