// // KBChatAssistantMessageCell.m // keyBoard // // Created by Kiro on 2026/1/23. // #import "KBChatAssistantMessageCell.h" #import "KBAiChatMessage.h" #import @interface KBChatAssistantMessageCell () @property (nonatomic, strong) UIButton *voiceButton; @property (nonatomic, strong) UILabel *durationLabel; @property (nonatomic, strong) UIView *bubbleView; @property (nonatomic, strong, readwrite) UILabel *messageLabel; // readwrite 允许内部修改 @property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator; @property (nonatomic, strong) KBAiChatMessage *currentMessage; // 打字机效果 @property (nonatomic, strong) NSTimer *typewriterTimer; @property (nonatomic, copy) NSString *fullText; @property (nonatomic, assign) NSInteger currentCharIndex; @end @implementation KBChatAssistantMessageCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.backgroundColor = [UIColor clearColor]; self.selectionStyle = UITableViewCellSelectionStyleNone; [self setupUI]; } return self; } - (void)setupUI { // 语音按钮 self.voiceButton = [UIButton buttonWithType:UIButtonTypeCustom]; [self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"] forState:UIControlStateNormal]; self.voiceButton.tintColor = [UIColor whiteColor]; [self.voiceButton addTarget:self action:@selector(voiceButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.contentView addSubview:self.voiceButton]; // 语音时长标签 self.durationLabel = [[UILabel alloc] init]; self.durationLabel.font = [UIFont systemFontOfSize:14]; self.durationLabel.textColor = [UIColor whiteColor]; [self.contentView addSubview:self.durationLabel]; // 加载指示器 self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; self.loadingIndicator.color = [UIColor whiteColor]; self.loadingIndicator.hidesWhenStopped = YES; [self.contentView addSubview:self.loadingIndicator]; // 气泡视图 self.bubbleView = [[UIView alloc] init]; self.bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7]; self.bubbleView.layer.cornerRadius = 16; self.bubbleView.layer.masksToBounds = YES; [self.contentView addSubview:self.bubbleView]; // 消息标签 self.messageLabel = [[UILabel alloc] init]; self.messageLabel.numberOfLines = 0; self.messageLabel.font = [UIFont systemFontOfSize:16]; self.messageLabel.textColor = [UIColor whiteColor]; // 设置 preferredMaxLayoutWidth 让 AutoLayout 能正确计算多行高度 CGFloat maxWidth = [UIScreen mainScreen].bounds.size.width * 0.75 - 16 - 24; self.messageLabel.preferredMaxLayoutWidth = maxWidth; [self.bubbleView addSubview:self.messageLabel]; // 布局约束 [self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(16); make.top.equalTo(self.contentView).offset(8); make.width.height.mas_equalTo(24); }]; [self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.voiceButton.mas_right).offset(4); make.centerY.equalTo(self.voiceButton); }]; [self.loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.voiceButton); }]; // bubbleView 约束 [self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.voiceButton.mas_bottom).offset(4); make.bottom.equalTo(self.contentView).offset(-4); make.left.equalTo(self.contentView).offset(16); make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75); }]; // messageLabel 约束 [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.bubbleView).offset(10); make.bottom.equalTo(self.bubbleView).offset(-10); make.left.equalTo(self.bubbleView).offset(12); make.right.equalTo(self.bubbleView).offset(-12); }]; } - (void)configureWithMessage:(KBAiChatMessage *)message { NSLog(@"[KBChatAssistantMessageCell] 配置消息 - 文本长度: %lu, isComplete: %d, needsTypewriter: %d, 打字机运行中: %d", (unsigned long)message.text.length, message.isComplete, message.needsTypewriterEffect, (self.typewriterTimer && self.typewriterTimer.isValid)); // 先停止之前的打字机效果(无论是否是同一条消息) [self stopTypewriterEffect]; self.currentMessage = message; // 只有明确需要打字机效果的消息才使用打字机 if (message.needsTypewriterEffect && !message.isComplete && message.text.length > 0) { // 启动新的打字机效果 NSLog(@"[KBChatAssistantMessageCell] 启动新的打字机效果"); [self startTypewriterEffectWithText:message.text]; } else { // 直接显示完整文本 NSLog(@"[KBChatAssistantMessageCell] 直接显示完整文本(needsTypewriter: %d, isComplete: %d)", message.needsTypewriterEffect, message.isComplete); self.messageLabel.text = message.text; } // 格式化语音时长(如果时长为 0,不显示) if (message.audioDuration > 0) { NSInteger seconds = (NSInteger)ceil(message.audioDuration); self.durationLabel.text = [NSString stringWithFormat:@"%ld\"", (long)seconds]; } else { self.durationLabel.text = @""; } // 如果有 audioId 或 audioData,显示语音按钮 BOOL hasAudio = (message.audioId.length > 0) || (message.audioData != nil && message.audioData.length > 0); self.voiceButton.hidden = !hasAudio; self.durationLabel.hidden = !hasAudio; } - (void)updateVoicePlayingState:(BOOL)isPlaying { if (isPlaying) { [self.voiceButton setImage:[UIImage systemImageNamed:@"pause.circle.fill"] forState:UIControlStateNormal]; } else { [self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"] forState:UIControlStateNormal]; } } - (void)showLoadingAnimation { // 隐藏按钮图标,显示加载动画 [self.voiceButton setImage:nil forState:UIControlStateNormal]; [self.loadingIndicator startAnimating]; } - (void)hideLoadingAnimation { // 停止加载动画,恢复按钮图标 [self.loadingIndicator stopAnimating]; [self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"] forState:UIControlStateNormal]; } - (void)voiceButtonTapped { if ([self.delegate respondsToSelector:@selector(assistantMessageCell:didTapVoiceButtonForMessage:)]) { [self.delegate assistantMessageCell:self didTapVoiceButtonForMessage:self.currentMessage]; } } #pragma mark - Typewriter Effect - (void)startTypewriterEffectWithText:(NSString *)text { if (text.length == 0) { NSLog(@"[KBChatAssistantMessageCell] 文本为空,跳过打字机效果"); return; } NSLog(@"[KBChatAssistantMessageCell] 开始打字机效果,文本长度: %lu, 文本: %@", (unsigned long)text.length, text); self.fullText = text; self.currentCharIndex = 0; // 先设置完整文本,让布局系统计算出正确的高度 self.messageLabel.text = text; // 强制布局更新 [self.messageLabel setNeedsLayout]; [self.bubbleView setNeedsLayout]; [self.contentView setNeedsLayout]; [self layoutIfNeeded]; NSLog(@"[KBChatAssistantMessageCell] 布局后 Label frame: %@", NSStringFromCGRect(self.messageLabel.frame)); // 使用 NSAttributedString 实现打字机效果 // 先把所有文字设置为透明,然后逐个显示 NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; [attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor clearColor] range:NSMakeRange(0, text.length)]; [attributedText addAttribute:NSFontAttributeName value:self.messageLabel.font range:NSMakeRange(0, text.length)]; self.messageLabel.attributedText = attributedText; // 确保在主线程创建定时器 dispatch_async(dispatch_get_main_queue(), ^{ // 每 0.03 秒显示一个字符(更快的打字机效果) self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03 target:self selector:@selector(typewriterTick) userInfo:nil repeats:YES]; // 将定时器添加到 RunLoop 的 common 模式,确保滚动时也能触发 [[NSRunLoop currentRunLoop] addTimer:self.typewriterTimer forMode:NSRunLoopCommonModes]; // 立即触发一次,显示第一个字符 [self typewriterTick]; NSLog(@"[KBChatAssistantMessageCell] 定时器已创建: %@", self.typewriterTimer); }); } - (void)typewriterTick { // 使用局部变量保存 fullText,避免多线程问题 NSString *text = self.fullText; // 检查 fullText 是否有效 if (!text || text.length == 0) { NSLog(@"[KBChatAssistantMessageCell] fullText 无效,停止打字机"); [self stopTypewriterEffect]; return; } // 如果当前索引小于完整文本长度,继续显示 if (self.currentCharIndex < text.length) { self.currentCharIndex++; // 使用 NSAttributedString 显示前 N 个字符,隐藏后面的字符 NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; // 已显示的部分:白色 if (self.currentCharIndex > 0) { [attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, self.currentCharIndex)]; } // 未显示的部分:透明 if (self.currentCharIndex < text.length) { [attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor clearColor] range:NSMakeRange(self.currentCharIndex, text.length - self.currentCharIndex)]; } // 设置字体 [attributedText addAttribute:NSFontAttributeName value:self.messageLabel.font range:NSMakeRange(0, text.length)]; self.messageLabel.attributedText = attributedText; // 每 10 个字符打印一次日志 if (self.currentCharIndex % 10 == 0) { NSString *displayText = [text substringToIndex:self.currentCharIndex]; NSLog(@"[KBChatAssistantMessageCell] 打字机进度: %ld/%lu, 当前文本: %@", (long)self.currentCharIndex, (unsigned long)text.length, displayText); NSLog(@"[KBChatAssistantMessageCell] Label 状态 - frame: %@, hidden: %d, alpha: %.2f", NSStringFromCGRect(self.messageLabel.frame), self.messageLabel.hidden, self.messageLabel.alpha); } } else { // 打字机效果已完成,停止定时器并标记消息完成 NSLog(@"[KBChatAssistantMessageCell] 打字机效果完成,停止定时器"); // 检查 text 是否为空 if (!text || text.length == 0) { NSLog(@"[KBChatAssistantMessageCell] text 为空,跳过"); [self stopTypewriterEffect]; return; } [self stopTypewriterEffect]; // 显示完整文本(白色),使用 attributedText 保持布局 NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; [attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, text.length)]; [attributedText addAttribute:NSFontAttributeName value:self.messageLabel.font range:NSMakeRange(0, text.length)]; self.messageLabel.attributedText = attributedText; // 标记消息为完成状态,避免 Cell 复用时重新触发打字机 if (self.currentMessage) { self.currentMessage.isComplete = YES; self.currentMessage.needsTypewriterEffect = NO; NSLog(@"[KBChatAssistantMessageCell] 标记消息完成 - 文本: %@", self.currentMessage.text); } } } - (void)stopTypewriterEffect { if (self.typewriterTimer && self.typewriterTimer.isValid) { NSLog(@"[KBChatAssistantMessageCell] 停止打字机定时器"); [self.typewriterTimer invalidate]; } self.typewriterTimer = nil; self.currentCharIndex = 0; self.fullText = nil; } - (void)prepareForReuse { [super prepareForReuse]; [self stopTypewriterEffect]; // 清空文本,避免复用时显示旧内容 self.messageLabel.text = @""; self.messageLabel.attributedText = nil; } - (void)dealloc { [self stopTypewriterEffect]; } @end