Files
keyboard/CustomKeyboard/View/Chat/KBChatAssistantCell.m
2026-01-30 13:33:23 +08:00

347 lines
12 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.

//
// 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);
}];
// 气泡
[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