2026-01-26 16:53:41 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBAIHomeVC.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Mac on 2026/1/26.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBAIHomeVC.h"
|
|
|
|
|
|
#import "KBPersonaChatCell.h"
|
|
|
|
|
|
#import "KBPersonaModel.h"
|
2026-01-26 18:17:02 +08:00
|
|
|
|
#import "KBVoiceInputBar.h"
|
2026-01-27 13:57:32 +08:00
|
|
|
|
#import "KBVoiceRecordManager.h"
|
2026-01-26 18:43:07 +08:00
|
|
|
|
#import "KBVoiceToTextManager.h"
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#import "AiVM.h"
|
2026-01-27 13:57:32 +08:00
|
|
|
|
#import "KBHUD.h"
|
2026-01-27 16:28:17 +08:00
|
|
|
|
#import "KBChatLimitPopView.h"
|
|
|
|
|
|
#import "KBVipPay.h"
|
|
|
|
|
|
#import "KBUserSessionManager.h"
|
|
|
|
|
|
#import "LSTPopView.h"
|
2026-01-28 16:35:47 +08:00
|
|
|
|
#import "KBAIMessageVC.h"
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#import <Masonry/Masonry.h>
|
2026-01-29 19:18:38 +08:00
|
|
|
|
#import <SDWebImage/SDWebImage.h>
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate, UITextViewDelegate>
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
|
|
|
|
|
/// 人设列表容器
|
|
|
|
|
|
@property (nonatomic, strong) UICollectionView *collectionView;
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
/// 底部语音输入栏
|
|
|
|
|
|
@property (nonatomic, strong) KBVoiceInputBar *voiceInputBar;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
@property (nonatomic, strong) MASConstraint *voiceInputBarBottomConstraint;
|
|
|
|
|
|
@property (nonatomic, assign) CGFloat voiceInputBarHeight;
|
|
|
|
|
|
@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing;
|
|
|
|
|
|
@property (nonatomic, assign) CGFloat currentKeyboardHeight;
|
2026-01-29 17:56:53 +08:00
|
|
|
|
/// 仅用于标记"由 KBVoiceInputBar 触发的键盘"是否处于激活态
|
2026-01-29 16:42:43 +08:00
|
|
|
|
@property (nonatomic, assign) BOOL voiceInputKeyboardActive;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
|
|
|
|
|
|
@property (nonatomic, weak) LSTPopView *chatLimitPopView;
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
/// 文本输入容器视图(键盘弹起时显示)
|
|
|
|
|
|
@property (nonatomic, strong) UIView *textInputContainerView;
|
|
|
|
|
|
/// 文本输入框
|
|
|
|
|
|
@property (nonatomic, strong) UITextView *textInputTextView;
|
|
|
|
|
|
/// 发送按钮
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *sendButton;
|
|
|
|
|
|
/// 占位符标签
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *placeholderLabel;
|
|
|
|
|
|
/// 文本输入容器底部约束
|
|
|
|
|
|
@property (nonatomic, strong) MASConstraint *textInputContainerBottomConstraint;
|
|
|
|
|
|
/// 是否处于文本输入模式
|
|
|
|
|
|
@property (nonatomic, assign) BOOL isTextInputMode;
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
/// 底部毛玻璃背景
|
|
|
|
|
|
@property (nonatomic, strong) UIView *bottomBackgroundView;
|
|
|
|
|
|
@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView;
|
|
|
|
|
|
@property (nonatomic, strong) CAGradientLayer *bottomMaskLayer;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-26 18:43:07 +08:00
|
|
|
|
/// 语音转写管理器
|
|
|
|
|
|
@property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager;
|
|
|
|
|
|
|
2026-01-27 13:57:32 +08:00
|
|
|
|
/// 录音管理器
|
|
|
|
|
|
@property (nonatomic, strong) KBVoiceRecordManager *voiceRecordManager;
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
/// 人设数据
|
|
|
|
|
|
@property (nonatomic, strong) NSMutableArray<KBPersonaModel *> *personas;
|
|
|
|
|
|
|
|
|
|
|
|
/// 当前页码
|
|
|
|
|
|
@property (nonatomic, assign) NSInteger currentPage;
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否还有更多数据
|
|
|
|
|
|
@property (nonatomic, assign) BOOL hasMore;
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否正在加载
|
|
|
|
|
|
@property (nonatomic, assign) BOOL isLoading;
|
|
|
|
|
|
|
|
|
|
|
|
/// 当前显示的索引
|
|
|
|
|
|
@property (nonatomic, assign) NSInteger currentIndex;
|
|
|
|
|
|
|
|
|
|
|
|
/// 已预加载的索引集合
|
|
|
|
|
|
@property (nonatomic, strong) NSMutableSet<NSNumber *> *preloadedIndexes;
|
|
|
|
|
|
|
|
|
|
|
|
/// AiVM 实例
|
|
|
|
|
|
@property (nonatomic, strong) AiVM *aiVM;
|
|
|
|
|
|
|
2026-01-27 17:03:16 +08:00
|
|
|
|
/// 是否正在等待 AI 回复(用于禁止滚动)
|
|
|
|
|
|
@property (nonatomic, assign) BOOL isWaitingForAIResponse;
|
|
|
|
|
|
|
2026-01-31 23:17:58 +08:00
|
|
|
|
@property (nonatomic, assign) NSInteger pendingAIRequestCount;
|
|
|
|
|
|
|
2026-01-28 16:35:47 +08:00
|
|
|
|
/// 右上角消息按钮
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *messageButton;
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBAIHomeVC
|
|
|
|
|
|
|
2026-01-29 16:42:43 +08:00
|
|
|
|
#pragma mark - Keyboard Gate
|
|
|
|
|
|
|
|
|
|
|
|
/// 查找当前 view 树里的 firstResponder
|
|
|
|
|
|
- (UIView *)kb_findFirstResponderInView:(UIView *)view {
|
|
|
|
|
|
if ([view isFirstResponder]) {
|
|
|
|
|
|
return view;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (UIView *sub in view.subviews) {
|
|
|
|
|
|
UIView *found = [self kb_findFirstResponderInView:sub];
|
|
|
|
|
|
if (found) {
|
|
|
|
|
|
return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
/// 仅允许 KBVoiceInputBar 或文本输入框触发键盘联动
|
2026-01-29 16:42:43 +08:00
|
|
|
|
- (BOOL)kb_isKeyboardFromVoiceInputBar {
|
|
|
|
|
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
|
|
|
|
|
if (!firstResponder) {
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
}
|
2026-01-29 17:56:53 +08:00
|
|
|
|
// 文本输入模式下,textInputTextView 也算
|
|
|
|
|
|
if (firstResponder == self.textInputTextView) {
|
|
|
|
|
|
return YES;
|
|
|
|
|
|
}
|
2026-01-29 16:42:43 +08:00
|
|
|
|
return [firstResponder isDescendantOfView:self.voiceInputBar];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
|
|
|
|
|
self.view.backgroundColor = [UIColor whiteColor];
|
|
|
|
|
|
self.kb_navView.hidden = true;
|
|
|
|
|
|
// 初始化数据
|
|
|
|
|
|
self.personas = [NSMutableArray array];
|
|
|
|
|
|
self.currentPage = 1;
|
|
|
|
|
|
self.hasMore = YES;
|
|
|
|
|
|
self.isLoading = NO;
|
|
|
|
|
|
self.currentIndex = 0;
|
|
|
|
|
|
self.preloadedIndexes = [NSMutableSet set];
|
|
|
|
|
|
self.aiVM = [[AiVM alloc] init];
|
2026-01-29 17:56:53 +08:00
|
|
|
|
self.isWaitingForAIResponse = NO;
|
2026-01-31 23:17:58 +08:00
|
|
|
|
self.pendingAIRequestCount = 0;
|
2026-01-29 17:56:53 +08:00
|
|
|
|
self.isTextInputMode = NO;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
|
|
|
|
|
[self setupUI];
|
2026-01-29 17:56:53 +08:00
|
|
|
|
[self setupTextInputView];
|
|
|
|
|
|
[self setupVoiceInputBarCallback];
|
2026-01-26 18:43:07 +08:00
|
|
|
|
[self setupVoiceToTextManager];
|
2026-01-27 13:57:32 +08:00
|
|
|
|
[self setupVoiceRecordManager];
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self setupKeyboardNotifications];
|
|
|
|
|
|
[self setupKeyboardDismissGesture];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
[self loadPersonas];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 14:29:42 +08:00
|
|
|
|
- (void)viewDidAppear:(BOOL)animated {
|
|
|
|
|
|
[super viewDidAppear:animated];
|
|
|
|
|
|
KBPersonaChatCell *cell = [self currentPersonaCell];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell onBecameCurrentPersonaCell];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
- (void)viewDidLayoutSubviews {
|
|
|
|
|
|
[super viewDidLayoutSubviews];
|
|
|
|
|
|
if (self.bottomMaskLayer) {
|
|
|
|
|
|
self.bottomMaskLayer.frame = self.bottomBlurEffectView.bounds;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#pragma mark - 1:控件初始化
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
2026-01-28 19:31:27 +08:00
|
|
|
|
self.voiceInputBarHeight = 80.0;
|
2026-01-28 17:21:19 +08:00
|
|
|
|
self.baseInputBarBottomSpacing = KB_TABBAR_HEIGHT;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
[self.view addSubview:self.collectionView];
|
|
|
|
|
|
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.view);
|
|
|
|
|
|
}];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-28 16:35:47 +08:00
|
|
|
|
// 右上角消息按钮
|
|
|
|
|
|
[self.view addSubview:self.messageButton];
|
|
|
|
|
|
[self.messageButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self.view).offset(KB_STATUSBAR_HEIGHT + 10);
|
|
|
|
|
|
make.right.equalTo(self.view).offset(-16);
|
|
|
|
|
|
make.width.height.mas_equalTo(32);
|
|
|
|
|
|
}];
|
2026-01-29 17:56:53 +08:00
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
// 底部毛玻璃背景
|
|
|
|
|
|
[self.view addSubview:self.bottomBackgroundView];
|
|
|
|
|
|
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
|
|
|
|
|
|
[self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.equalTo(self.view);
|
|
|
|
|
|
make.bottom.equalTo(self.view);
|
|
|
|
|
|
make.height.mas_equalTo(self.voiceInputBarHeight);
|
|
|
|
|
|
}];
|
|
|
|
|
|
[self.bottomBlurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.bottomBackgroundView);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
// 底部语音输入栏
|
|
|
|
|
|
[self.view addSubview:self.voiceInputBar];
|
|
|
|
|
|
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.equalTo(self.view);
|
2026-01-27 16:28:17 +08:00
|
|
|
|
self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
2026-01-29 17:56:53 +08:00
|
|
|
|
make.height.mas_equalTo(self.voiceInputBarHeight);
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
/// 设置文本输入视图
|
|
|
|
|
|
- (void)setupTextInputView {
|
|
|
|
|
|
// 文本输入容器(初始隐藏)
|
|
|
|
|
|
[self.view addSubview:self.textInputContainerView];
|
|
|
|
|
|
[self.textInputContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.equalTo(self.view);
|
|
|
|
|
|
self.textInputContainerBottomConstraint = make.bottom.equalTo(self.view).offset(100); // 初始在屏幕外
|
|
|
|
|
|
make.height.mas_greaterThanOrEqualTo(50);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.textInputContainerView addSubview:self.textInputTextView];
|
|
|
|
|
|
[self.textInputContainerView addSubview:self.sendButton];
|
|
|
|
|
|
[self.textInputTextView addSubview:self.placeholderLabel];
|
|
|
|
|
|
|
|
|
|
|
|
[self.sendButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.right.equalTo(self.textInputContainerView).offset(-16);
|
|
|
|
|
|
make.bottom.equalTo(self.textInputContainerView).offset(-10);
|
|
|
|
|
|
make.width.mas_equalTo(60);
|
|
|
|
|
|
make.height.mas_equalTo(36);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.textInputTextView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.textInputContainerView).offset(16);
|
|
|
|
|
|
make.right.equalTo(self.sendButton.mas_left).offset(-10);
|
|
|
|
|
|
make.top.equalTo(self.textInputContainerView).offset(8);
|
|
|
|
|
|
make.bottom.equalTo(self.textInputContainerView).offset(-8);
|
|
|
|
|
|
make.height.mas_greaterThanOrEqualTo(36);
|
|
|
|
|
|
make.height.mas_lessThanOrEqualTo(100);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
2026-01-30 21:24:17 +08:00
|
|
|
|
make.left.equalTo(self.textInputTextView).offset(15);
|
2026-01-29 17:56:53 +08:00
|
|
|
|
make.top.equalTo(self.textInputTextView).offset(8);
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 设置 VoiceInputBar 的文本发送回调
|
|
|
|
|
|
- (void)setupVoiceInputBarCallback {
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
self.voiceInputBar.onTextSend = ^(NSString *text) {
|
|
|
|
|
|
// 文本模式下点击,显示文本输入框
|
|
|
|
|
|
[weakSelf showTextInputView];
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 显示文本输入视图
|
|
|
|
|
|
- (void)showTextInputView {
|
|
|
|
|
|
self.isTextInputMode = YES;
|
|
|
|
|
|
self.voiceInputBar.hidden = YES;
|
|
|
|
|
|
self.textInputContainerView.hidden = NO;
|
|
|
|
|
|
[self.textInputTextView becomeFirstResponder];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 隐藏文本输入视图
|
|
|
|
|
|
- (void)hideTextInputView {
|
|
|
|
|
|
self.isTextInputMode = NO;
|
|
|
|
|
|
[self.textInputTextView resignFirstResponder];
|
|
|
|
|
|
self.textInputContainerView.hidden = YES;
|
|
|
|
|
|
self.voiceInputBar.hidden = NO;
|
|
|
|
|
|
self.textInputTextView.text = @"";
|
|
|
|
|
|
self.placeholderLabel.hidden = NO;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#pragma mark - 2:数据加载
|
|
|
|
|
|
|
|
|
|
|
|
- (void)loadPersonas {
|
|
|
|
|
|
if (self.isLoading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.isLoading = YES;
|
|
|
|
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[self.aiVM fetchPersonasWithPageNum:self.currentPage
|
|
|
|
|
|
pageSize:10
|
|
|
|
|
|
completion:^(KBPersonaPageModel * _Nullable pageModel, NSError * _Nullable error) {
|
|
|
|
|
|
weakSelf.isLoading = NO;
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
NSLog(@"加载人设列表失败:%@", error.localizedDescription);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!pageModel || !pageModel.records) {
|
|
|
|
|
|
NSLog(@"人设列表数据为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[weakSelf.personas addObjectsFromArray:pageModel.records];
|
|
|
|
|
|
weakSelf.hasMore = pageModel.hasMore;
|
|
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
[weakSelf.collectionView reloadData];
|
|
|
|
|
|
if (weakSelf.currentPage == 1) {
|
|
|
|
|
|
[weakSelf preloadDataForIndexes:@[@0, @1, @2]];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@",
|
2026-01-29 17:56:53 +08:00
|
|
|
|
weakSelf.personas.count, pageModel.total, pageModel.hasMore ? @"是" : @"否");
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)loadMorePersonas {
|
|
|
|
|
|
if (!self.hasMore || self.isLoading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.currentPage++;
|
|
|
|
|
|
[self loadPersonas];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - 3:预加载逻辑
|
|
|
|
|
|
|
|
|
|
|
|
- (void)preloadAdjacentCellsForIndex:(NSInteger)index {
|
|
|
|
|
|
if (index < 0 || index >= self.personas.count) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSMutableArray *indexesToPreload = [NSMutableArray array];
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
[indexesToPreload addObject:@(index - 1)];
|
|
|
|
|
|
}
|
|
|
|
|
|
[indexesToPreload addObject:@(index)];
|
|
|
|
|
|
if (index < self.personas.count - 1) {
|
|
|
|
|
|
[indexesToPreload addObject:@(index + 1)];
|
|
|
|
|
|
}
|
|
|
|
|
|
[self preloadDataForIndexes:indexesToPreload];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)preloadDataForIndexes:(NSArray<NSNumber *> *)indexes {
|
|
|
|
|
|
for (NSNumber *indexNum in indexes) {
|
|
|
|
|
|
if ([self.preloadedIndexes containsObject:indexNum]) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
[self.preloadedIndexes addObject:indexNum];
|
|
|
|
|
|
|
|
|
|
|
|
NSInteger index = [indexNum integerValue];
|
|
|
|
|
|
if (index >= self.personas.count) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
|
|
|
|
|
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell preloadDataIfNeeded];
|
|
|
|
|
|
}
|
|
|
|
|
|
NSLog(@"预加载第 %ld 个人设", (long)index);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - UICollectionViewDataSource
|
|
|
|
|
|
|
|
|
|
|
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
|
|
|
|
|
return self.personas.count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
|
|
|
|
|
cell.persona = self.personas[indexPath.item];
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self updateChatViewBottomInset];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
[self.preloadedIndexes addObject:@(indexPath.item)];
|
|
|
|
|
|
[cell preloadDataIfNeeded];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - UIScrollViewDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
2026-01-27 17:03:16 +08:00
|
|
|
|
if (self.isWaitingForAIResponse) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
|
|
|
|
|
CGFloat offsetY = scrollView.contentOffset.y;
|
|
|
|
|
|
NSInteger currentPage = offsetY / pageHeight;
|
|
|
|
|
|
|
|
|
|
|
|
if (fmod(offsetY, pageHeight) > pageHeight * 0.3) {
|
|
|
|
|
|
[self preloadAdjacentCellsForIndex:currentPage + 1];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[self preloadAdjacentCellsForIndex:currentPage];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) {
|
|
|
|
|
|
[self loadMorePersonas];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
|
|
|
|
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
|
|
|
|
|
NSInteger currentPage = scrollView.contentOffset.y / pageHeight;
|
2026-02-02 14:29:42 +08:00
|
|
|
|
NSInteger previousIndex = self.currentIndex;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
self.currentIndex = currentPage;
|
|
|
|
|
|
|
2026-02-02 14:29:42 +08:00
|
|
|
|
if (previousIndex != self.currentIndex) {
|
|
|
|
|
|
NSIndexPath *prevPath = [NSIndexPath indexPathForItem:previousIndex inSection:0];
|
|
|
|
|
|
KBPersonaChatCell *prevCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:prevPath];
|
|
|
|
|
|
if (prevCell) {
|
|
|
|
|
|
[prevCell onResignedCurrentPersonaCell];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
|
|
|
|
|
if (currentCell) {
|
|
|
|
|
|
[currentCell onBecameCurrentPersonaCell];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
|
|
|
|
|
if (currentCell) {
|
|
|
|
|
|
[currentCell onBecameCurrentPersonaCell];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
if (currentPage < self.personas.count) {
|
|
|
|
|
|
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
2026-01-29 19:18:38 +08:00
|
|
|
|
// 保存当前选中的 persona 到 AppGroup,供键盘扩展使用
|
|
|
|
|
|
[self saveSelectedPersonaToAppGroup:self.personas[currentPage]];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self updateChatViewBottomInset];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 19:18:38 +08:00
|
|
|
|
#pragma mark - AppGroup Persona 共享
|
|
|
|
|
|
|
|
|
|
|
|
/// 保存选中的 persona 到 AppGroup,供键盘扩展读取
|
|
|
|
|
|
- (void)saveSelectedPersonaToAppGroup:(KBPersonaModel *)persona {
|
|
|
|
|
|
if (!persona) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
|
|
|
|
|
if (!ud) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 无法访问 AppGroup");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存 persona 的关键信息
|
|
|
|
|
|
NSDictionary *personaDict = @{
|
|
|
|
|
|
@"personaId": @(persona.personaId),
|
|
|
|
|
|
@"name": persona.name ?: @"",
|
|
|
|
|
|
@"avatarUrl": persona.avatarUrl ?: @"",
|
|
|
|
|
|
@"coverImageUrl": persona.coverImageUrl ?: @"",
|
|
|
|
|
|
@"shortDesc": persona.shortDesc ?: @""
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
[ud setObject:personaDict forKey:@"AppGroup_SelectedPersona"];
|
|
|
|
|
|
[ud synchronize];
|
|
|
|
|
|
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 已保存选中的 persona 到 AppGroup: %@, coverImageUrl: %@", persona.name, persona.coverImageUrl);
|
|
|
|
|
|
|
|
|
|
|
|
// 异步下载并缩小图片,保存到 AppGroup 共享目录
|
|
|
|
|
|
[self downloadAndSavePersonaCoverImage:persona.coverImageUrl];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 下载并缩小 persona 封面图,保存到 AppGroup 共享目录
|
|
|
|
|
|
- (void)downloadAndSavePersonaCoverImage:(NSString *)imageUrl {
|
|
|
|
|
|
if (imageUrl.length == 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 AppGroup 共享目录
|
|
|
|
|
|
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
|
|
|
|
|
if (!containerURL) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 无法获取 AppGroup 容器目录");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSString *imagePath = [[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"];
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 SDWebImage 下载图片
|
|
|
|
|
|
[[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:imageUrl]
|
|
|
|
|
|
options:SDWebImageHighPriority
|
|
|
|
|
|
progress:nil
|
|
|
|
|
|
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
|
|
|
|
|
|
if (error || !image) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 下载 persona 封面图失败: %@", error.localizedDescription);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 缩小图片到适合键盘扩展的尺寸(宽度 390,高度按比例)
|
|
|
|
|
|
CGFloat targetWidth = 390.0;
|
|
|
|
|
|
CGFloat scale = targetWidth / image.size.width;
|
|
|
|
|
|
CGSize targetSize = CGSizeMake(targetWidth, image.size.height * scale);
|
|
|
|
|
|
|
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(targetSize, YES, 1.0);
|
|
|
|
|
|
[image drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)];
|
|
|
|
|
|
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
|
|
|
|
|
|
|
|
// 压缩为 JPEG,质量 0.6
|
|
|
|
|
|
NSData *jpegData = UIImageJPEGRepresentation(scaledImage, 0.6);
|
|
|
|
|
|
if (!jpegData) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 压缩图片失败");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到 AppGroup 共享目录
|
|
|
|
|
|
BOOL success = [jpegData writeToFile:imagePath atomically:YES];
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] persona 封面图已保存到: %@, 大小: %lu KB", imagePath, (unsigned long)jpegData.length / 1024);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 保存 persona 封面图失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 17:03:16 +08:00
|
|
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
|
|
|
|
|
if (self.isWaitingForAIResponse) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动");
|
|
|
|
|
|
scrollView.scrollEnabled = NO;
|
|
|
|
|
|
scrollView.scrollEnabled = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:43:07 +08:00
|
|
|
|
#pragma mark - 4:语音转写
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupVoiceToTextManager {
|
|
|
|
|
|
self.voiceToTextManager = [[KBVoiceToTextManager alloc] initWithInputBar:self.voiceInputBar];
|
|
|
|
|
|
self.voiceToTextManager.delegate = self;
|
2026-01-27 13:57:32 +08:00
|
|
|
|
self.voiceToTextManager.deepgramEnabled = NO;
|
2026-01-26 18:43:07 +08:00
|
|
|
|
[self.voiceToTextManager prepareConnection];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 13:57:32 +08:00
|
|
|
|
/// 5:录音管理
|
|
|
|
|
|
- (void)setupVoiceRecordManager {
|
|
|
|
|
|
self.voiceRecordManager = [[KBVoiceRecordManager alloc] init];
|
|
|
|
|
|
self.voiceRecordManager.delegate = self;
|
|
|
|
|
|
self.voiceRecordManager.minRecordDuration = 1.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
#pragma mark - 6:键盘监听
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupKeyboardNotifications {
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
|
selector:@selector(handleKeyboardWillChangeFrame:)
|
|
|
|
|
|
name:UIKeyboardWillChangeFrameNotification
|
|
|
|
|
|
object:nil];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleKeyboardWillChangeFrame:(NSNotification *)notification {
|
|
|
|
|
|
NSDictionary *userInfo = notification.userInfo;
|
|
|
|
|
|
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
|
|
|
|
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
|
|
|
|
UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16);
|
|
|
|
|
|
|
|
|
|
|
|
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
|
|
|
|
|
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
2026-01-29 16:42:43 +08:00
|
|
|
|
|
|
|
|
|
|
if (keyboardHeight > 0.0) {
|
|
|
|
|
|
if (![self kb_isKeyboardFromVoiceInputBar]) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.voiceInputKeyboardActive = YES;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!self.voiceInputKeyboardActive) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.voiceInputKeyboardActive = NO;
|
2026-01-29 17:56:53 +08:00
|
|
|
|
// 键盘隐藏时,如果是文本输入模式,隐藏文本输入框并显示 VoiceInputBar
|
|
|
|
|
|
if (self.isTextInputMode) {
|
|
|
|
|
|
[self hideTextInputView];
|
|
|
|
|
|
}
|
2026-01-29 16:42:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
self.currentKeyboardHeight = keyboardHeight;
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
NSLog(@"[KBAIHomeVC] 键盘高度: %.2f", keyboardHeight);
|
|
|
|
|
|
|
2026-01-28 17:21:19 +08:00
|
|
|
|
CGFloat bottomSpacing;
|
|
|
|
|
|
if (keyboardHeight > 0.0) {
|
|
|
|
|
|
bottomSpacing = keyboardHeight - 5.0;
|
2026-01-29 17:56:53 +08:00
|
|
|
|
// 文本输入模式:更新文本输入容器位置
|
|
|
|
|
|
if (self.isTextInputMode) {
|
|
|
|
|
|
[self.textInputContainerBottomConstraint setOffset:-keyboardHeight];
|
|
|
|
|
|
}
|
2026-01-28 17:21:19 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
bottomSpacing = self.baseInputBarBottomSpacing;
|
2026-01-29 17:56:53 +08:00
|
|
|
|
[self.textInputContainerBottomConstraint setOffset:100]; // 移出屏幕
|
2026-01-28 17:21:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self.voiceInputBarBottomConstraint setOffset:-bottomSpacing];
|
|
|
|
|
|
[self updateChatViewBottomInset];
|
|
|
|
|
|
|
|
|
|
|
|
[UIView animateWithDuration:duration
|
|
|
|
|
|
delay:0
|
|
|
|
|
|
options:options
|
|
|
|
|
|
animations:^{
|
|
|
|
|
|
[self.view layoutIfNeeded];
|
|
|
|
|
|
}
|
|
|
|
|
|
completion:nil];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - 7:键盘收起
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupKeyboardDismissGesture {
|
|
|
|
|
|
self.dismissKeyboardTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
|
|
|
|
|
action:@selector(handleBackgroundTap)];
|
|
|
|
|
|
self.dismissKeyboardTap.cancelsTouchesInView = NO;
|
|
|
|
|
|
self.dismissKeyboardTap.delegate = self;
|
|
|
|
|
|
[self.view addGestureRecognizer:self.dismissKeyboardTap];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleBackgroundTap {
|
|
|
|
|
|
[self.view endEditing:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - UIGestureRecognizerDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
|
|
|
|
|
if ([touch.view isDescendantOfView:self.voiceInputBar]) {
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
}
|
2026-01-29 17:56:53 +08:00
|
|
|
|
if ([touch.view isDescendantOfView:self.textInputContainerView]) {
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
return YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:51:37 +08:00
|
|
|
|
- (NSInteger)currentCompanionId {
|
|
|
|
|
|
if (self.personas.count == 0) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSInteger index = self.currentIndex;
|
|
|
|
|
|
if (index < 0 || index >= self.personas.count) {
|
|
|
|
|
|
NSIndexPath *indexPath = self.collectionView.indexPathsForVisibleItems.firstObject;
|
|
|
|
|
|
if (indexPath) {
|
|
|
|
|
|
index = indexPath.item;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
index = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KBPersonaModel *persona = self.personas[index];
|
|
|
|
|
|
return persona.personaId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (KBPersonaChatCell *)currentPersonaCell {
|
|
|
|
|
|
if (self.personas.count == 0) {
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0];
|
|
|
|
|
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (NSIndexPath *visibleIndex in self.collectionView.indexPathsForVisibleItems) {
|
|
|
|
|
|
KBPersonaChatCell *visibleCell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:visibleIndex];
|
|
|
|
|
|
if (visibleCell) {
|
|
|
|
|
|
return visibleCell;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
#pragma mark - Private
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateChatViewBottomInset {
|
2026-01-28 17:21:19 +08:00
|
|
|
|
CGFloat bottomInset;
|
|
|
|
|
|
|
|
|
|
|
|
if (self.currentKeyboardHeight > 0.0) {
|
2026-01-29 17:56:53 +08:00
|
|
|
|
CGFloat avatarBottomSpace = KB_TABBAR_HEIGHT + 50 + 20;
|
|
|
|
|
|
CGFloat chatViewPhysicalBottomSpace = avatarBottomSpace + 54 + 10;
|
2026-01-28 17:21:19 +08:00
|
|
|
|
bottomInset = (self.currentKeyboardHeight + self.voiceInputBarHeight) - chatViewPhysicalBottomSpace;
|
2026-01-29 13:13:42 +08:00
|
|
|
|
bottomInset = MAX(bottomInset, 0);
|
2026-01-28 17:21:19 +08:00
|
|
|
|
} else {
|
2026-01-29 13:13:42 +08:00
|
|
|
|
bottomInset = 0;
|
2026-01-28 17:21:19 +08:00
|
|
|
|
}
|
2026-01-27 18:53:19 +08:00
|
|
|
|
|
2026-01-28 17:21:19 +08:00
|
|
|
|
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
|
|
|
|
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell updateChatViewBottomInset:bottomInset];
|
|
|
|
|
|
if (self.currentKeyboardHeight > 0.0) {
|
|
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
2026-01-29 13:13:42 +08:00
|
|
|
|
[cell.chatView scrollToBottom];
|
2026-01-28 17:21:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)showChatLimitPopWithMessage:(NSString *)message {
|
|
|
|
|
|
if (self.chatLimitPopView) {
|
|
|
|
|
|
[self.chatLimitPopView dismiss];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CGFloat width = KB_SCREEN_WIDTH - 60;
|
|
|
|
|
|
KBChatLimitPopView *content = [[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)];
|
|
|
|
|
|
content.message = message;
|
|
|
|
|
|
content.delegate = self;
|
|
|
|
|
|
|
|
|
|
|
|
LSTPopView *pop = [LSTPopView initWithCustomView:content
|
|
|
|
|
|
parentView:nil
|
|
|
|
|
|
popStyle:LSTPopStyleFade
|
|
|
|
|
|
dismissStyle:LSTDismissStyleFade];
|
|
|
|
|
|
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
|
|
|
|
|
pop.hemStyle = LSTHemStyleCenter;
|
|
|
|
|
|
pop.isClickBgDismiss = YES;
|
|
|
|
|
|
pop.isAvoidKeyboard = NO;
|
|
|
|
|
|
self.chatLimitPopView = pop;
|
|
|
|
|
|
[pop pop];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
#pragma mark - UITextViewDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)textViewDidChange:(UITextView *)textView {
|
|
|
|
|
|
self.placeholderLabel.hidden = textView.text.length > 0;
|
|
|
|
|
|
// 动态调整高度
|
|
|
|
|
|
CGSize size = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
|
|
|
|
|
|
CGFloat newHeight = MIN(MAX(size.height, 36), 100);
|
|
|
|
|
|
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.height.mas_greaterThanOrEqualTo(newHeight);
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#pragma mark - Lazy Load
|
|
|
|
|
|
|
|
|
|
|
|
- (UICollectionView *)collectionView {
|
|
|
|
|
|
if (!_collectionView) {
|
|
|
|
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
|
|
|
|
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
|
|
|
|
|
layout.minimumLineSpacing = 0;
|
|
|
|
|
|
layout.minimumInteritemSpacing = 0;
|
|
|
|
|
|
layout.itemSize = [UIScreen mainScreen].bounds.size;
|
|
|
|
|
|
|
|
|
|
|
|
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
|
|
|
|
_collectionView.pagingEnabled = YES;
|
|
|
|
|
|
_collectionView.showsVerticalScrollIndicator = NO;
|
|
|
|
|
|
_collectionView.backgroundColor = [UIColor whiteColor];
|
|
|
|
|
|
_collectionView.delegate = self;
|
|
|
|
|
|
_collectionView.dataSource = self;
|
|
|
|
|
|
[_collectionView registerClass:[KBPersonaChatCell class] forCellWithReuseIdentifier:@"KBPersonaChatCell"];
|
|
|
|
|
|
|
|
|
|
|
|
if (@available(iOS 11.0, *)) {
|
|
|
|
|
|
_collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return _collectionView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
- (KBVoiceInputBar *)voiceInputBar {
|
|
|
|
|
|
if (!_voiceInputBar) {
|
|
|
|
|
|
_voiceInputBar = [[KBVoiceInputBar alloc] init];
|
|
|
|
|
|
_voiceInputBar.statusText = @"按住按钮开始对话";
|
|
|
|
|
|
}
|
|
|
|
|
|
return _voiceInputBar;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
- (UIView *)textInputContainerView {
|
|
|
|
|
|
if (!_textInputContainerView) {
|
|
|
|
|
|
_textInputContainerView = [[UIView alloc] init];
|
|
|
|
|
|
_textInputContainerView.backgroundColor = [UIColor whiteColor];
|
|
|
|
|
|
_textInputContainerView.hidden = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _textInputContainerView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UITextView *)textInputTextView {
|
|
|
|
|
|
if (!_textInputTextView) {
|
|
|
|
|
|
_textInputTextView = [[UITextView alloc] init];
|
|
|
|
|
|
_textInputTextView.font = [UIFont systemFontOfSize:16];
|
|
|
|
|
|
_textInputTextView.textColor = [UIColor blackColor];
|
|
|
|
|
|
_textInputTextView.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0];
|
|
|
|
|
|
_textInputTextView.layer.cornerRadius = 18;
|
|
|
|
|
|
_textInputTextView.layer.masksToBounds = YES;
|
|
|
|
|
|
_textInputTextView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
|
|
|
|
|
|
_textInputTextView.delegate = self;
|
|
|
|
|
|
_textInputTextView.returnKeyType = UIReturnKeySend;
|
|
|
|
|
|
_textInputTextView.enablesReturnKeyAutomatically = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _textInputTextView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)sendButton {
|
|
|
|
|
|
if (!_sendButton) {
|
|
|
|
|
|
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
|
[_sendButton setTitle:KBLocalized(@"发送") forState:UIControlStateNormal];
|
|
|
|
|
|
[_sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
|
|
|
|
|
_sendButton.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
|
|
|
|
|
_sendButton.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:1.0];
|
|
|
|
|
|
_sendButton.layer.cornerRadius = 18;
|
|
|
|
|
|
_sendButton.layer.masksToBounds = YES;
|
|
|
|
|
|
[_sendButton addTarget:self action:@selector(sendButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _sendButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UILabel *)placeholderLabel {
|
|
|
|
|
|
if (!_placeholderLabel) {
|
|
|
|
|
|
_placeholderLabel = [[UILabel alloc] init];
|
|
|
|
|
|
_placeholderLabel.text = KBLocalized(@"输入消息...");
|
|
|
|
|
|
_placeholderLabel.font = [UIFont systemFontOfSize:16];
|
|
|
|
|
|
_placeholderLabel.textColor = [UIColor lightGrayColor];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _placeholderLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
#pragma mark - KBChatLimitPopViewDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
|
|
|
|
|
[self.chatLimitPopView dismiss];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
|
|
|
|
|
[self.chatLimitPopView dismiss];
|
|
|
|
|
|
if (![KBUserSessionManager shared].isLoggedIn) {
|
|
|
|
|
|
[[KBUserSessionManager shared] goLoginVC];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
KBVipPay *vc = [[KBVipPay alloc] init];
|
|
|
|
|
|
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)bottomBackgroundView {
|
|
|
|
|
|
if (!_bottomBackgroundView) {
|
|
|
|
|
|
_bottomBackgroundView = [[UIView alloc] init];
|
|
|
|
|
|
_bottomBackgroundView.clipsToBounds = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _bottomBackgroundView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIVisualEffectView *)bottomBlurEffectView {
|
|
|
|
|
|
if (!_bottomBlurEffectView) {
|
|
|
|
|
|
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
|
|
|
|
|
_bottomBlurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
|
|
|
|
|
_bottomBlurEffectView.layer.mask = self.bottomMaskLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _bottomBlurEffectView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (CAGradientLayer *)bottomMaskLayer {
|
|
|
|
|
|
if (!_bottomMaskLayer) {
|
|
|
|
|
|
_bottomMaskLayer = [CAGradientLayer layer];
|
|
|
|
|
|
_bottomMaskLayer.startPoint = CGPointMake(0.5, 1);
|
|
|
|
|
|
_bottomMaskLayer.endPoint = CGPointMake(0.5, 0);
|
|
|
|
|
|
_bottomMaskLayer.colors = @[
|
|
|
|
|
|
(__bridge id)[UIColor whiteColor].CGColor,
|
|
|
|
|
|
(__bridge id)[UIColor whiteColor].CGColor,
|
|
|
|
|
|
(__bridge id)[UIColor clearColor].CGColor
|
|
|
|
|
|
];
|
|
|
|
|
|
_bottomMaskLayer.locations = @[@(0.0), @(0.5), @(1.0)];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _bottomMaskLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 16:35:47 +08:00
|
|
|
|
- (UIButton *)messageButton {
|
|
|
|
|
|
if (!_messageButton) {
|
|
|
|
|
|
_messageButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
|
[_messageButton setImage:[UIImage imageNamed:@"ai_message_icon"] forState:UIControlStateNormal];
|
|
|
|
|
|
[_messageButton addTarget:self action:@selector(messageButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _messageButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
|
|
|
|
|
|
- (void)messageButtonTapped {
|
|
|
|
|
|
KBAIMessageVC *vc = [[KBAIMessageVC alloc] init];
|
|
|
|
|
|
[self.navigationController pushViewController:vc animated:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:56:53 +08:00
|
|
|
|
/// 发送按钮点击 - 直接调用 handleTranscribedText
|
|
|
|
|
|
- (void)sendButtonTapped {
|
|
|
|
|
|
NSString *text = [self.textInputTextView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
|
|
|
|
if (text.length == 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空输入框
|
|
|
|
|
|
self.textInputTextView.text = @"";
|
|
|
|
|
|
self.placeholderLabel.hidden = NO;
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏键盘和文本输入框
|
|
|
|
|
|
[self hideTextInputView];
|
|
|
|
|
|
|
|
|
|
|
|
// 直接调用 handleTranscribedText,不走语音录制流程
|
|
|
|
|
|
[self handleTranscribedText:text];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:43:07 +08:00
|
|
|
|
#pragma mark - KBVoiceToTextManagerDelegate
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-26 18:43:07 +08:00
|
|
|
|
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
|
|
|
|
|
didReceiveFinalText:(NSString *)text {
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self handleTranscribedText:text];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
|
|
|
|
|
didFailWithError:(NSError *)error {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 语音识别失败:%@", error.localizedDescription);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceToTextManagerDidBeginRecording:(KBVoiceToTextManager *)manager {
|
|
|
|
|
|
[self.voiceRecordManager startRecording];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceToTextManagerDidEndRecording:(KBVoiceToTextManager *)manager {
|
|
|
|
|
|
[self.voiceRecordManager stopRecording];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceToTextManagerDidCancelRecording:(KBVoiceToTextManager *)manager {
|
|
|
|
|
|
[self.voiceRecordManager cancelRecording];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - KBVoiceRecordManagerDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
|
|
|
|
|
|
didFinishRecordingAtURL:(NSURL *)fileURL
|
|
|
|
|
|
duration:(NSTimeInterval)duration {
|
|
|
|
|
|
NSDictionary *attributes = [[NSFileManager defaultManager]
|
|
|
|
|
|
attributesOfItemAtPath:fileURL.path
|
|
|
|
|
|
error:nil];
|
|
|
|
|
|
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
|
|
|
|
|
|
2026-01-29 14:42:49 +08:00
|
|
|
|
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
|
|
|
|
|
if (currentCell) {
|
|
|
|
|
|
[currentCell appendLoadingUserMessage];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[self.aiVM transcribeAudioFileAtURL:fileURL
|
|
|
|
|
|
completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) {
|
|
|
|
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
|
|
|
|
if (!strongSelf) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2026-01-29 14:42:49 +08:00
|
|
|
|
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
if (error) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription);
|
|
|
|
|
|
[KBHUD showError:KBLocalized(@"语音转文字失败,请重试")];
|
2026-01-29 14:42:49 +08:00
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell updateLastUserMessage:KBLocalized(@"语音识别失败")];
|
|
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSString *transcript = response.data.transcript ?: @"";
|
|
|
|
|
|
if (transcript.length == 0) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
|
|
|
|
|
|
[KBHUD showError:KBLocalized(@"未识别到语音内容")];
|
2026-01-29 14:42:49 +08:00
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell updateLastUserMessage:KBLocalized(@"未识别到语音")];
|
|
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 14:42:49 +08:00
|
|
|
|
if (cell) {
|
|
|
|
|
|
[cell updateLastUserMessage:transcript];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[strongSelf handleTranscribedText:transcript appendToUI:NO];
|
2026-01-27 16:28:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 录音过短,已忽略");
|
|
|
|
|
|
[KBHUD showError:KBLocalized(@"录音时间过短,请重新录音")];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
|
|
|
|
|
|
didFailWithError:(NSError *)error {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Private
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleTranscribedText:(NSString *)text {
|
2026-01-29 14:42:49 +08:00
|
|
|
|
[self handleTranscribedText:text appendToUI:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleTranscribedText:(NSString *)text appendToUI:(BOOL)appendToUI {
|
2026-01-26 18:43:07 +08:00
|
|
|
|
if (text.length == 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-29 17:56:53 +08:00
|
|
|
|
NSLog(@"[KBAIHomeVC] 发送消息:%@", text);
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-26 18:51:37 +08:00
|
|
|
|
NSInteger companionId = [self currentCompanionId];
|
|
|
|
|
|
if (companionId <= 0) {
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] companionId 无效,取消请求");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
2026-01-31 23:17:58 +08:00
|
|
|
|
NSString *requestId = [NSUUID UUID].UUIDString;
|
2026-01-29 14:42:49 +08:00
|
|
|
|
if (currentCell && appendToUI) {
|
2026-01-31 23:17:58 +08:00
|
|
|
|
[currentCell appendUserMessage:text requestId:requestId];
|
|
|
|
|
|
[currentCell appendLoadingAssistantMessageWithRequestId:requestId];
|
2026-01-26 20:36:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 23:17:58 +08:00
|
|
|
|
self.pendingAIRequestCount += 1;
|
|
|
|
|
|
self.isWaitingForAIResponse = (self.pendingAIRequestCount > 0);
|
|
|
|
|
|
if (self.pendingAIRequestCount == 1) {
|
|
|
|
|
|
self.collectionView.scrollEnabled = NO;
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动");
|
|
|
|
|
|
}
|
2026-01-27 17:03:16 +08:00
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
__weak typeof(self) weakSelf = self;
|
2026-01-26 18:51:37 +08:00
|
|
|
|
[self.aiVM requestChatMessageWithContent:text
|
|
|
|
|
|
companionId:companionId
|
|
|
|
|
|
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
|
2026-01-26 20:36:51 +08:00
|
|
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
|
|
|
|
if (!strongSelf) {
|
2026-01-26 18:51:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-26 20:36:51 +08:00
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2026-01-31 23:17:58 +08:00
|
|
|
|
if (strongSelf.pendingAIRequestCount > 0) {
|
|
|
|
|
|
strongSelf.pendingAIRequestCount -= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
strongSelf.isWaitingForAIResponse = (strongSelf.pendingAIRequestCount > 0);
|
|
|
|
|
|
if (strongSelf.pendingAIRequestCount == 0) {
|
|
|
|
|
|
strongSelf.collectionView.scrollEnabled = YES;
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
|
|
|
|
|
|
}
|
2026-01-29 14:42:49 +08:00
|
|
|
|
|
|
|
|
|
|
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
2026-01-27 16:28:17 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.code == 50030) {
|
2026-01-29 20:56:24 +08:00
|
|
|
|
// 移除 loading 消息
|
|
|
|
|
|
if (cell) {
|
2026-01-31 23:17:58 +08:00
|
|
|
|
[cell removeLoadingAssistantMessageWithRequestId:requestId];
|
2026-01-29 20:56:24 +08:00
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
NSString *message = response.message ?: @"";
|
|
|
|
|
|
[strongSelf showChatLimitPopWithMessage:message];
|
2026-01-26 20:36:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!response || !response.data) {
|
2026-01-29 20:56:24 +08:00
|
|
|
|
// 移除 loading 消息
|
|
|
|
|
|
if (cell) {
|
2026-01-31 23:17:58 +08:00
|
|
|
|
[cell removeLoadingAssistantMessageWithRequestId:requestId];
|
2026-01-29 20:56:24 +08:00
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
NSString *message = response.message ?: @"聊天响应为空";
|
|
|
|
|
|
NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message);
|
|
|
|
|
|
if (message.length > 0) {
|
|
|
|
|
|
[KBHUD showError:message];
|
|
|
|
|
|
}
|
2026-01-26 20:36:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
|
|
|
|
|
NSString *audioId = response.data.audioId;
|
|
|
|
|
|
if (aiResponse.length == 0) {
|
2026-01-29 20:56:24 +08:00
|
|
|
|
// 移除 loading 消息
|
|
|
|
|
|
if (cell) {
|
2026-01-31 23:17:58 +08:00
|
|
|
|
[cell removeLoadingAssistantMessageWithRequestId:requestId];
|
2026-01-29 20:56:24 +08:00
|
|
|
|
}
|
2026-01-26 20:36:51 +08:00
|
|
|
|
NSLog(@"[KBAIHomeVC] AI 回复为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cell) {
|
2026-01-31 23:17:58 +08:00
|
|
|
|
[cell updateAssistantMessageWithRequestId:requestId text:aiResponse audioId:audioId];
|
2026-01-26 20:36:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-26 18:51:37 +08:00
|
|
|
|
}];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
- (void)dealloc {
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
2026-01-27 13:57:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
@end
|