1
This commit is contained in:
@@ -13,15 +13,30 @@
|
||||
#import "KBVoiceToTextManager.h"
|
||||
#import "AiVM.h"
|
||||
#import "KBHUD.h"
|
||||
#import "KBChatLimitPopView.h"
|
||||
#import "KBVipPay.h"
|
||||
#import "KBUserSessionManager.h"
|
||||
#import "LSTPopView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate>
|
||||
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate>
|
||||
|
||||
/// 人设列表容器
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
|
||||
/// 底部语音输入栏
|
||||
@property (nonatomic, strong) KBVoiceInputBar *voiceInputBar;
|
||||
@property (nonatomic, strong) MASConstraint *voiceInputBarBottomConstraint;
|
||||
@property (nonatomic, assign) CGFloat voiceInputBarHeight;
|
||||
@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing;
|
||||
@property (nonatomic, assign) CGFloat currentKeyboardHeight;
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
|
||||
@property (nonatomic, weak) LSTPopView *chatLimitPopView;
|
||||
|
||||
/// 底部毛玻璃背景
|
||||
@property (nonatomic, strong) UIView *bottomBackgroundView;
|
||||
@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView;
|
||||
@property (nonatomic, strong) CAGradientLayer *bottomMaskLayer;
|
||||
|
||||
/// 语音转写管理器
|
||||
@property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager;
|
||||
@@ -72,23 +87,47 @@
|
||||
[self setupUI];
|
||||
[self setupVoiceToTextManager];
|
||||
[self setupVoiceRecordManager];
|
||||
[self setupKeyboardNotifications];
|
||||
[self setupKeyboardDismissGesture];
|
||||
[self loadPersonas];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
if (self.bottomMaskLayer) {
|
||||
self.bottomMaskLayer.frame = self.bottomBlurEffectView.bounds;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 1:控件初始化
|
||||
|
||||
- (void)setupUI {
|
||||
self.voiceInputBarHeight = 150.0;
|
||||
self.baseInputBarBottomSpacing = 20.0;
|
||||
[self.view addSubview:self.collectionView];
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 底部毛玻璃背景
|
||||
[self.view addSubview:self.bottomBackgroundView];
|
||||
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
|
||||
[self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
// self.bottomBackgroundBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
||||
make.bottom.equalTo(self.view);
|
||||
make.height.mas_equalTo(self.voiceInputBarHeight);
|
||||
}];
|
||||
[self.bottomBlurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.bottomBackgroundView);
|
||||
}];
|
||||
|
||||
// 底部语音输入栏
|
||||
[self.view addSubview:self.voiceInputBar];
|
||||
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
make.bottom.equalTo(self.view).offset(-20);
|
||||
make.height.mas_equalTo(150); // 根据实际需要调整高度
|
||||
self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
||||
make.height.mas_equalTo(self.voiceInputBarHeight); // 根据实际需要调整高度
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -206,6 +245,7 @@
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
||||
cell.persona = self.personas[indexPath.item];
|
||||
[self updateChatViewBottomInset];
|
||||
|
||||
// 标记为已预加载
|
||||
[self.preloadedIndexes addObject:@(indexPath.item)];
|
||||
@@ -244,6 +284,8 @@
|
||||
if (currentPage < self.personas.count) {
|
||||
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
||||
}
|
||||
|
||||
[self updateChatViewBottomInset];
|
||||
}
|
||||
|
||||
#pragma mark - 4:语音转写
|
||||
@@ -262,6 +304,61 @@
|
||||
self.voiceRecordManager.minRecordDuration = 1.0;
|
||||
}
|
||||
|
||||
#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));
|
||||
self.currentKeyboardHeight = keyboardHeight;
|
||||
|
||||
CGFloat bottomSpacing = (keyboardHeight > 0.0) ? (keyboardHeight + 8.0) : self.baseInputBarBottomSpacing;
|
||||
[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;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSInteger)currentCompanionId {
|
||||
if (self.personas.count == 0) {
|
||||
return 0;
|
||||
@@ -302,6 +399,42 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)updateChatViewBottomInset {
|
||||
CGFloat bottomSpacing = (self.currentKeyboardHeight > 0.0) ? (self.currentKeyboardHeight + 8.0) : self.baseInputBarBottomSpacing;
|
||||
CGFloat bottomInset = self.voiceInputBarHeight + bottomSpacing;
|
||||
|
||||
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
||||
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||
if (cell) {
|
||||
[cell updateChatViewBottomInset:bottomInset];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Load
|
||||
|
||||
- (UICollectionView *)collectionView {
|
||||
@@ -335,59 +468,59 @@
|
||||
return _voiceInputBar;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#pragma mark - KBVoiceToTextManagerDelegate
|
||||
|
||||
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
||||
didReceiveFinalText:(NSString *)text {
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text);
|
||||
|
||||
NSInteger companionId = [self currentCompanionId];
|
||||
if (companionId <= 0) {
|
||||
NSLog(@"[KBAIHomeVC] companionId 无效,取消请求");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
[currentCell appendUserMessage:text];
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM requestChatMessageWithContent:text
|
||||
companionId:companionId
|
||||
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.data) {
|
||||
NSLog(@"[KBAIHomeVC] 聊天响应为空");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
||||
NSString *audioId = response.data.audioId;
|
||||
if (aiResponse.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] AI 回复为空");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell appendAssistantMessage:aiResponse audioId:audioId];
|
||||
}
|
||||
});
|
||||
}];
|
||||
[self handleTranscribedText:text];
|
||||
}
|
||||
|
||||
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
||||
@@ -417,6 +550,32 @@
|
||||
error:nil];
|
||||
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
||||
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
||||
|
||||
__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(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription);
|
||||
[KBHUD showError:KBLocalized(@"语音转文字失败,请重试")];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *transcript = response.data.transcript ?: @"";
|
||||
if (transcript.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
|
||||
[KBHUD showError:KBLocalized(@"未识别到语音内容")];
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf handleTranscribedText:transcript];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager {
|
||||
@@ -429,4 +588,72 @@
|
||||
NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription);
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)handleTranscribedText:(NSString *)text {
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text);
|
||||
|
||||
NSInteger companionId = [self currentCompanionId];
|
||||
if (companionId <= 0) {
|
||||
NSLog(@"[KBAIHomeVC] companionId 无效,取消请求");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
[currentCell appendUserMessage:text];
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM requestChatMessageWithContent:text
|
||||
companionId:companionId
|
||||
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// if (error) {
|
||||
// NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (response.code == 50030) {
|
||||
NSString *message = response.message ?: @"";
|
||||
[strongSelf showChatLimitPopWithMessage:message];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.data) {
|
||||
NSString *message = response.message ?: @"聊天响应为空";
|
||||
NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message);
|
||||
if (message.length > 0) {
|
||||
[KBHUD showError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
||||
NSString *audioId = response.data.audioId;
|
||||
if (aiResponse.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] AI 回复为空");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell appendAssistantMessage:aiResponse audioId:audioId];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user