diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index 1309c7c..8717e56 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -1073,7 +1073,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, __strong typeof(weakSelf) self = weakSelf; if (!self) return; - if (!response.success) { + if (response.code != 0) { if (response.code == 50030) { NSLog(@"[KB] ⚠️ 次数用尽: %@", response.message); [self.chatPanelView kb_removeLoadingAssistantMessage]; @@ -1086,9 +1086,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, return; } - NSLog(@"[KB] ✅ 收到回复: %@", response.text); + NSLog(@"[KB] ✅ 收到回复: %@", response.data.aiResponse); - if (response.text.length == 0) { + if (response.data.aiResponse.length == 0) { [self.chatPanelView kb_removeLoadingAssistantMessage]; [KBHUD showInfo:KBLocalized(@"未获取到回复内容")]; return; @@ -1096,12 +1096,12 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, // 添加 AI 消息(带打字机效果) NSLog(@"[KB] 准备添加 AI 消息"); - [self.chatPanelView kb_addAssistantMessage:response.text audioId:response.audioId]; + [self.chatPanelView kb_addAssistantMessage:response.data.aiResponse audioId:response.data.audioId]; NSLog(@"[KB] AI 消息添加完成"); // 如果有 audioId,开始预加载音频 - if (response.audioId.length > 0) { - [self kb_preloadAudioWithAudioId:response.audioId]; + if (response.data.audioId.length > 0) { + [self kb_preloadAudioWithAudioId:response.data.audioId]; } }]; } diff --git a/CustomKeyboard/VM/KBVM.h b/CustomKeyboard/VM/KBVM.h index b961be2..12eb1d4 100644 --- a/CustomKeyboard/VM/KBVM.h +++ b/CustomKeyboard/VM/KBVM.h @@ -7,17 +7,25 @@ #import #import - +@class KBChatDataModel; NS_ASSUME_NONNULL_BEGIN /// 聊天响应模型 @interface KBChatResponse : NSObject -@property (nonatomic, copy, nullable) NSString *text; -@property (nonatomic, copy, nullable) NSString *audioId; +@property (nonatomic, strong, nullable) KBChatDataModel *data; +//@property (nonatomic, copy, nullable) NSString *audioId; @property (nonatomic, copy, nullable) NSString *message; @property (nonatomic, assign) BOOL success; @property (nonatomic, assign) NSInteger code; +@end + +@interface KBChatDataModel : NSObject +@property (nonatomic, copy, nullable) NSString *aiResponse; +@property (nonatomic, copy, nullable) NSString *audioId; +@property (nonatomic, copy, nullable) NSString *llmDuration; + + @end /// 音频响应模型 diff --git a/CustomKeyboard/VM/KBVM.m b/CustomKeyboard/VM/KBVM.m index ddf4f80..287e077 100644 --- a/CustomKeyboard/VM/KBVM.m +++ b/CustomKeyboard/VM/KBVM.m @@ -12,6 +12,9 @@ @implementation KBChatResponse @end +@implementation KBChatDataModel +@end + @implementation KBAudioResponse @end @@ -68,8 +71,7 @@ completion:^(NSDictionary *json, NSURLResponse *response, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ KBChatResponse *chatResponse = [KBChatResponse mj_objectWithKeyValues:json]; - - if (error) { + if (chatResponse.code != 0) { chatResponse.success = NO; // chatResponse.errorMessage = error.localizedDescription ?: @"请求失败"; if (completion) completion(chatResponse); diff --git a/CustomKeyboard/View/KBToolBar.h b/CustomKeyboard/View/KBToolBar.h index 1b8eeab..5bb4553 100644 --- a/CustomKeyboard/View/KBToolBar.h +++ b/CustomKeyboard/View/KBToolBar.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak, nullable) id delegate; -/// 左侧按钮的标题(数量由数组决定)。默认值:@[@"AI", @"语音"]。 +/// 左侧按钮的标题(数量由数组决定)。默认值:@[@"AI"]。 @property (nonatomic, copy) NSArray *leftButtonTitles; /// 暴露按钮以便外部定制(只读;首次访问时懒加载创建) diff --git a/CustomKeyboard/View/KBToolBar.m b/CustomKeyboard/View/KBToolBar.m index b15106b..9b8d8f2 100644 --- a/CustomKeyboard/View/KBToolBar.m +++ b/CustomKeyboard/View/KBToolBar.m @@ -15,23 +15,25 @@ @property (nonatomic, strong) NSArray *leftButtonsInternal; //@property (nonatomic, strong) UIButton *settingsButtonInternal; @property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键 +@property (nonatomic, strong) UIImageView *avatarImageView; // 右侧头像(AppGroup persona_cover.jpg) @property (nonatomic, strong) UIButton *undoButtonInternal; // 右侧撤销删除 @property (nonatomic, assign) BOOL kbNeedsInputModeSwitchKey; @property (nonatomic, assign) BOOL kbUndoVisible; +@property (nonatomic, assign) BOOL kbAvatarVisible; @end @implementation KBToolBar -static NSString * const kKBAIKeyIdentifier = @"ai"; -static NSString * const kKBUndoKeyIdentifier = @"key_revoke"; -static const CGFloat kKBAIButtonWidth = 40; -static const CGFloat kKBAIButtonHeight = 40; -static const NSInteger kKBVoiceButtonIndex = 1; + static NSString * const kKBAIKeyIdentifier = @"ai"; + static NSString * const kKBUndoKeyIdentifier = @"key_revoke"; + static const CGFloat kKBAIButtonWidth = 40; + static const CGFloat kKBAIButtonHeight = 40; + static const CGFloat kKBAvatarSize = 40; - (instancetype)initWithFrame:(CGRect)frame{ if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; - _leftButtonTitles = @[@"AI", KBLocalized(@"语音")]; // 默认标题 + _leftButtonTitles = @[@"AI"]; // 默认标题 [self setupUI]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_undoStateChanged) @@ -69,7 +71,6 @@ static const NSInteger kKBVoiceButtonIndex = 1; } }]; [self kb_updateAIButtonAppearance]; - [self kb_updateVoiceButtonAppearance]; } #pragma mark - 视图搭建 @@ -79,6 +80,7 @@ static const NSInteger kKBVoiceButtonIndex = 1; // [self addSubview:self.settingsButtonInternal]; [self addSubview:self.globeButtonInternal]; [self addSubview:self.undoButtonInternal]; + [self addSubview:self.avatarImageView]; // 右侧设置按钮 // [self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { @@ -94,13 +96,7 @@ static const NSInteger kKBVoiceButtonIndex = 1; make.width.height.mas_equalTo(32); }]; - // 右侧撤销按钮 - [self.undoButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(self.mas_right).offset(-12); - make.centerY.equalTo(self.mas_centerY); - make.height.mas_equalTo(32); - make.width.mas_equalTo(84); - }]; + [self kb_updateRightControlsConstraints]; [self kb_updateLeftContainerConstraints]; @@ -173,8 +169,8 @@ static const NSInteger kKBVoiceButtonIndex = 1; - (void)kb_applyTheme { [self kb_updateAIButtonAppearance]; - [self kb_updateVoiceButtonAppearance]; [self kb_updateUndoButtonAppearance]; + [self kb_updateAvatarAppearance]; } - (void)kb_updateAIButtonAppearance { @@ -211,16 +207,6 @@ static const NSInteger kKBVoiceButtonIndex = 1; } } -- (void)kb_updateVoiceButtonAppearance { - UIButton *voiceButton = [self kb_voiceButton]; - if (!voiceButton) { return; } - - voiceButton.backgroundColor = [UIColor colorWithHex:0xE53935]; - [voiceButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - voiceButton.layer.cornerRadius = 16; - voiceButton.layer.masksToBounds = YES; -} - - (void)kb_updateUndoButtonAppearance { if (!self.undoButtonInternal) { return; } @@ -241,6 +227,41 @@ static const NSInteger kKBVoiceButtonIndex = 1; } } +#pragma mark - Avatar + +- (void)kb_updateAvatarAppearance { + UIImage *img = [self kb_personaCoverImageFromAppGroup]; + BOOL shouldShow = (img != nil); + self.avatarImageView.image = img; + if (self.kbAvatarVisible == shouldShow) { + self.avatarImageView.hidden = !shouldShow; + return; + } + self.kbAvatarVisible = shouldShow; + self.avatarImageView.hidden = !shouldShow; + [self kb_updateRightControlsConstraints]; + [self kb_updateLeftContainerConstraints]; + [self setNeedsLayout]; + [self layoutIfNeeded]; +} + +- (nullable UIImage *)kb_personaCoverImageFromAppGroup { + NSURL *containerURL = [[NSFileManager defaultManager] + containerURLForSecurityApplicationGroupIdentifier:AppGroup]; + if (!containerURL) { + return nil; + } + + NSString *imagePath = + [[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"]; + if (imagePath.length == 0 || + ![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) { + return nil; + } + + return [UIImage imageWithContentsOfFile:imagePath]; +} + #pragma mark - Actions - (void)onLeftAction:(UIButton *)sender { @@ -261,6 +282,16 @@ static const NSInteger kKBVoiceButtonIndex = 1; } } +- (void)onAvatarTap { + if (!self.kbAvatarVisible || self.avatarImageView.hidden) { + return; + } + // 复用原“语音”入口的 index=1 逻辑(外部会按 index 做面板切换) + if ([self.delegate respondsToSelector:@selector(toolBar:didTapActionAtIndex:)]) { + [self.delegate toolBar:self didTapActionAtIndex:1]; + } +} + #pragma mark - Lazy - (UIView *)leftContainer { @@ -271,6 +302,23 @@ static const NSInteger kKBVoiceButtonIndex = 1; return _leftContainer; } +- (UIImageView *)avatarImageView { + if (!_avatarImageView) { + _avatarImageView = [[UIImageView alloc] init]; + _avatarImageView.hidden = YES; + _avatarImageView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9]; + _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; + _avatarImageView.layer.cornerRadius = kKBAvatarSize * 0.5; + _avatarImageView.layer.masksToBounds = YES; + _avatarImageView.userInteractionEnabled = YES; + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(onAvatarTap)]; + [_avatarImageView addGestureRecognizer:tap]; + } + return _avatarImageView; +} + //- (UIButton *)settingsButtonInternal { // if (!_settingsButtonInternal) { // _settingsButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem]; @@ -319,11 +367,6 @@ static const NSInteger kKBVoiceButtonIndex = 1; return self.leftButtonsInternal[0]; } -- (UIButton *)kb_voiceButton { - if (self.leftButtonsInternal.count <= kKBVoiceButtonIndex) { return nil; } - return self.leftButtonsInternal[kKBVoiceButtonIndex]; -} - #pragma mark - Globe (Input Mode Switch) // 根据宿主是否已提供系统切换键,决定是否显示地球按钮;并绑定系统事件。 @@ -362,6 +405,8 @@ static const NSInteger kKBVoiceButtonIndex = 1; } if (self.kbUndoVisible) { make.right.equalTo(self.undoButtonInternal.mas_left).offset(-8); + } else if (self.kbAvatarVisible) { + make.right.equalTo(self.avatarImageView.mas_left).offset(-8); } else { make.right.equalTo(self).offset(-12); } @@ -371,6 +416,24 @@ static const NSInteger kKBVoiceButtonIndex = 1; }]; } +- (void)kb_updateRightControlsConstraints { + [self.avatarImageView mas_remakeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self).offset(-12); + make.centerY.equalTo(self).offset(5); + make.width.height.mas_equalTo(kKBAvatarSize); + }]; + [self.undoButtonInternal mas_remakeConstraints:^(MASConstraintMaker *make) { + if (self.kbAvatarVisible) { + make.right.equalTo(self.avatarImageView.mas_left).offset(-8); + } else { + make.right.equalTo(self).offset(-12); + } + make.centerY.equalTo(self.mas_centerY); + make.height.mas_equalTo(32); + make.width.mas_equalTo(84); + }]; +} + - (void)kb_undoStateChanged { [self kb_updateUndoVisibilityAnimated:YES]; } @@ -405,6 +468,7 @@ static const NSInteger kKBVoiceButtonIndex = 1; - (void)didMoveToWindow { [super didMoveToWindow]; [self kb_refreshGlobeVisibility]; + [self kb_updateAvatarAppearance]; } @end