// // KBChatAssistantCell.m // CustomKeyboard // // AI 消息 Cell(左侧显示,带语音按钮和打字机效果) // #import "KBChatAssistantCell.h" #import "KBChatMessage.h" #import "Masonry.h" @interface KBChatAssistantCell () @property (nonatomic, strong) UIButton *voiceButton; @property (nonatomic, strong) UILabel *durationLabel; @property (nonatomic, strong) UIView *bubbleView; @property (nonatomic, strong) UILabel *messageLabel; @property (nonatomic, strong) UIActivityIndicatorView *voiceLoadingIndicator; @property (nonatomic, strong) UIActivityIndicatorView *messageLoadingIndicator; @property (nonatomic, strong) KBChatMessage *currentMessage; /// 打字机效果 @property (nonatomic, strong) NSTimer *typewriterTimer; @property (nonatomic, copy) NSString *fullText; @property (nonatomic, assign) NSInteger currentCharIndex; @end @implementation KBChatAssistantCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.backgroundColor = [UIColor clearColor]; self.contentView.backgroundColor = [UIColor clearColor]; self.selectionStyle = UITableViewCellSelectionStyleNone; [self setupUI]; } return self; } - (void)setupUI { [self.contentView addSubview:self.voiceButton]; [self.contentView addSubview:self.durationLabel]; [self.contentView addSubview:self.voiceLoadingIndicator]; [self.contentView addSubview:self.messageLoadingIndicator]; [self.contentView addSubview:self.bubbleView]; [self.bubbleView addSubview:self.messageLabel]; // 语音按钮 [self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(12); make.top.equalTo(self.contentView).offset(6); make.width.height.mas_equalTo(20); }]; // 语音时长 [self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.voiceButton.mas_right).offset(4); make.centerY.equalTo(self.voiceButton); }]; // 语音加载指示器 [self.voiceLoadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.voiceButton); }]; // 消息加载指示器 [self.messageLoadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(12); make.top.equalTo(self.voiceButton.mas_bottom).offset(8); }]; // 气泡 [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(12); make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.7); }]; // 消息文本 [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.bubbleView).offset(8); make.bottom.equalTo(self.bubbleView).offset(-8); make.left.equalTo(self.bubbleView).offset(12); make.right.equalTo(self.bubbleView).offset(-12); make.height.greaterThanOrEqualTo(@18); }]; } - (void)configureWithMessage:(KBChatMessage *)message { NSLog(@"[KBChatAssistantCell] ========== configureWithMessage =========="); NSLog(@"[KBChatAssistantCell] text: %@", message.text); NSLog(@"[KBChatAssistantCell] outgoing: %d, isLoading: %d, isComplete: %d, needsTypewriter: %d", message.outgoing, message.isLoading, message.isComplete, message.needsTypewriterEffect); // 先停止之前的打字机效果 [self stopTypewriterEffect]; self.currentMessage = message; // 处理 loading 状态 if (message.isLoading) { NSLog(@"[KBChatAssistantCell] 显示 loading 状态"); self.messageLabel.attributedText = nil; self.messageLabel.text = @""; self.bubbleView.hidden = YES; self.voiceButton.hidden = YES; self.durationLabel.hidden = YES; [self.messageLoadingIndicator startAnimating]; return; } // 非 loading 状态 [self.messageLoadingIndicator stopAnimating]; self.bubbleView.hidden = NO; // 语音按钮显示逻辑 BOOL hasAudio = (message.audioId.length > 0) || (message.audioData.length > 0); self.voiceButton.hidden = !hasAudio; self.durationLabel.hidden = !hasAudio; NSLog(@"[KBChatAssistantCell] hasAudio: %d, audioId: %@", hasAudio, message.audioId); // 语音时长 if (message.audioDuration > 0) { NSInteger seconds = (NSInteger)ceil(message.audioDuration); self.durationLabel.text = [NSString stringWithFormat:@"%ld\"", (long)seconds]; } else { self.durationLabel.text = @""; } // 打字机效果 if (message.needsTypewriterEffect && !message.isComplete && message.text.length > 0) { NSLog(@"[KBChatAssistantCell] ✅ 启动打字机效果"); [self startTypewriterEffectWithText:message.text]; } else { NSLog(@"[KBChatAssistantCell] 直接显示文本(不使用打字机)"); self.messageLabel.attributedText = nil; self.messageLabel.text = message.text ?: @""; } } #pragma mark - Typewriter Effect - (void)startTypewriterEffectWithText:(NSString *)text { if (text.length == 0) return; self.fullText = text; self.currentCharIndex = 0; // 先设置完整文本让布局计算正确高度 self.messageLabel.text = text; [self.contentView setNeedsLayout]; [self.contentView layoutIfNeeded]; // 应用打字机效果 dispatch_async(dispatch_get_main_queue(), ^{ 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; self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03 target:self selector:@selector(typewriterTick) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.typewriterTimer forMode:NSRunLoopCommonModes]; [self typewriterTick]; }); } - (void)typewriterTick { NSString *text = self.fullText; if (!text || text.length == 0) { [self stopTypewriterEffect]; return; } if (self.currentCharIndex < text.length) { self.currentCharIndex++; NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; UIColor *textColor = [UIColor whiteColor]; if (self.currentCharIndex > 0) { [attributedText addAttribute:NSForegroundColorAttributeName value:textColor 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; } else { [self stopTypewriterEffect]; // 显示完整文本 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; // 标记完成 if (self.currentMessage) { self.currentMessage.isComplete = YES; self.currentMessage.needsTypewriterEffect = NO; } } } - (void)stopTypewriterEffect { if (self.typewriterTimer && self.typewriterTimer.isValid) { [self.typewriterTimer invalidate]; } self.typewriterTimer = nil; self.currentCharIndex = 0; self.fullText = nil; } #pragma mark - Voice Button - (void)updateVoicePlayingState:(BOOL)isPlaying { UIImage *icon = nil; if (@available(iOS 13.0, *)) { icon = isPlaying ? [UIImage systemImageNamed:@"pause.circle.fill"] : [UIImage systemImageNamed:@"play.circle.fill"]; } [self.voiceButton setImage:icon forState:UIControlStateNormal]; } - (void)showVoiceLoadingAnimation { [self.voiceButton setImage:nil forState:UIControlStateNormal]; [self.voiceLoadingIndicator startAnimating]; } - (void)hideVoiceLoadingAnimation { [self.voiceLoadingIndicator stopAnimating]; UIImage *icon = nil; if (@available(iOS 13.0, *)) { icon = [UIImage systemImageNamed:@"play.circle.fill"]; } [self.voiceButton setImage:icon forState:UIControlStateNormal]; } - (void)voiceButtonTapped { if ([self.delegate respondsToSelector:@selector(assistantCell:didTapVoiceButtonForMessage:)]) { [self.delegate assistantCell:self didTapVoiceButtonForMessage:self.currentMessage]; } } #pragma mark - Reuse - (void)prepareForReuse { [super prepareForReuse]; [self stopTypewriterEffect]; self.messageLabel.text = @""; self.messageLabel.attributedText = nil; [self.messageLoadingIndicator stopAnimating]; [self.voiceLoadingIndicator stopAnimating]; } - (void)dealloc { [self stopTypewriterEffect]; } #pragma mark - Lazy - (UIButton *)voiceButton { if (!_voiceButton) { _voiceButton = [UIButton buttonWithType:UIButtonTypeCustom]; UIImage *icon = nil; if (@available(iOS 13.0, *)) { icon = [UIImage systemImageNamed:@"play.circle.fill"]; } [_voiceButton setImage:icon forState:UIControlStateNormal]; _voiceButton.tintColor = [UIColor whiteColor]; [_voiceButton addTarget:self action:@selector(voiceButtonTapped) forControlEvents:UIControlEventTouchUpInside]; } return _voiceButton; } - (UILabel *)durationLabel { if (!_durationLabel) { _durationLabel = [[UILabel alloc] init]; _durationLabel.font = [UIFont systemFontOfSize:11]; _durationLabel.textColor = [UIColor whiteColor]; } return _durationLabel; } - (UIActivityIndicatorView *)voiceLoadingIndicator { if (!_voiceLoadingIndicator) { _voiceLoadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; _voiceLoadingIndicator.color = [UIColor whiteColor]; _voiceLoadingIndicator.hidesWhenStopped = YES; } return _voiceLoadingIndicator; } - (UIActivityIndicatorView *)messageLoadingIndicator { if (!_messageLoadingIndicator) { _messageLoadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; _messageLoadingIndicator.color = [UIColor whiteColor]; _messageLoadingIndicator.hidesWhenStopped = YES; } return _messageLoadingIndicator; } - (UIView *)bubbleView { if (!_bubbleView) { _bubbleView = [[UIView alloc] init]; _bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7]; _bubbleView.layer.cornerRadius = 12; _bubbleView.layer.masksToBounds = YES; } return _bubbleView; } - (UILabel *)messageLabel { if (!_messageLabel) { _messageLabel = [[UILabel alloc] init]; _messageLabel.numberOfLines = 0; _messageLabel.font = [UIFont systemFontOfSize:14]; _messageLabel.textColor = [UIColor whiteColor]; _messageLabel.lineBreakMode = NSLineBreakByWordWrapping; } return _messageLabel; } @end