Files
keyboard/keyBoard/Class/AiTalk/VC/KBAiMainVC.m
2026-01-19 19:14:20 +08:00

383 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.

//
// KBAiMainVC.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiMainVC.h"
#import "ConversationOrchestrator.h"
#import "KBAICommentView.h"
#import "KBAiChatView.h"
#import "KBAiRecordButton.h"
#import "LSTPopView.h"
@interface KBAiMainVC () <KBAiRecordButtonDelegate>
@property(nonatomic, weak) LSTPopView *popView;
// UI
@property(nonatomic, strong) KBAiChatView *chatView;
@property(nonatomic, strong) KBAiRecordButton *recordButton;
@property(nonatomic, strong) UILabel *statusLabel;
@property(nonatomic, strong) UIButton *commentButton;
@property(nonatomic, strong) KBAICommentView *commentView;
@property(nonatomic, strong) UIView *tabbarBackgroundView;
@property(nonatomic, strong) UIVisualEffectView *blurEffectView;
@property(nonatomic, strong) CAGradientLayer *gradientLayer;
@property(nonatomic, strong) UIImageView *personImageView;
// 核心模块
@property(nonatomic, strong) ConversationOrchestrator *orchestrator;
@end
@implementation KBAiMainVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
// 让视图延伸到屏幕边缘(包括状态栏和导航栏下方)
self.edgesForExtendedLayout = UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars = YES;
[self setupUI];
[self setupOrchestrator];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// TabBar 背景色由 BaseTabBarController 统一管理,这里不需要设置
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 页面消失时停止对话
[self.orchestrator stop];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 只更新 mask 的 framemask 已在 setupUI 中创建)
if (self.blurEffectView.layer.mask) {
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
}
}
#pragma mark - UI Setup
- (void)setupUI {
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"AI 助手";
// 安全区域
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
// PersonImageView背景图最底层
self.personImageView =
[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"person_icon"]];
[self.view addSubview:self.personImageView];
[self.personImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.bottom.equalTo(self.view);
}];
// TabBar 毛玻璃模糊背景(在 personImageView 之上)
self.tabbarBackgroundView = [[UIView alloc] init];
self.tabbarBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
self.tabbarBackgroundView.clipsToBounds = YES;
[self.view addSubview:self.tabbarBackgroundView];
// 模糊效果
UIBlurEffect *blurEffect =
[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
self.blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
self.blurEffectView.translatesAutoresizingMaskIntoConstraints = NO;
[self.tabbarBackgroundView addSubview:self.blurEffectView];
// 为 blurEffectView 创建透明度渐变
// mask从底部到中间不透明从中间到顶部透明
CAGradientLayer *maskLayer = [CAGradientLayer layer];
maskLayer.startPoint = CGPointMake(0.5, 1); // 底部
maskLayer.endPoint = CGPointMake(0.5, 0); // 顶部
// 底部到中间保持不透明,从中间到顶部过渡透明
maskLayer.colors = @[
(__bridge id)[UIColor whiteColor].CGColor, // 底部:完全不透明
(__bridge id)[UIColor whiteColor].CGColor, // 中间:完全不透明
(__bridge id)[UIColor clearColor].CGColor // 顶部:完全透明
];
maskLayer.locations = @[ @(0.0), @(0.5), @(1.0) ];
self.blurEffectView.layer.mask = maskLayer;
// 状态标签
self.statusLabel = [[UILabel alloc] init];
self.statusLabel.text = @"按住按钮开始对话";
self.statusLabel.font = [UIFont systemFontOfSize:14];
self.statusLabel.textColor = [UIColor secondaryLabelColor];
self.statusLabel.textAlignment = NSTextAlignmentCenter;
self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.statusLabel];
// 聊天视图
// self.chatView = [[KBAiChatView alloc] init];
// self.chatView.backgroundColor = [UIColor systemBackgroundColor];
// self.chatView.translatesAutoresizingMaskIntoConstraints = NO;
// [self.view addSubview:self.chatView];
// 录音按钮
self.recordButton = [[KBAiRecordButton alloc] init];
self.recordButton.delegate = self;
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.recordButton];
// 评论按钮(聊天视图右侧居中)
self.commentButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.commentButton setImage:[UIImage systemImageNamed:@"bubble.right.fill"]
forState:UIControlStateNormal];
self.commentButton.tintColor = [UIColor whiteColor];
self.commentButton.backgroundColor = [UIColor systemBlueColor];
self.commentButton.layer.cornerRadius = 25;
self.commentButton.layer.shadowColor = [UIColor blackColor].CGColor;
self.commentButton.layer.shadowOffset = CGSizeMake(0, 2);
self.commentButton.layer.shadowOpacity = 0.3;
self.commentButton.layer.shadowRadius = 4;
self.commentButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.commentButton addTarget:self
action:@selector(showComment)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.commentButton];
// 布局约束 - 使用 Masonry
[self.tabbarBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self.view);
make.height.mas_equalTo(KBFit(238));
}];
[self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.tabbarBackgroundView);
}];
[self.statusLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(8);
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
}];
[self.recordButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft).offset(20);
make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight).offset(-20);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-16);
make.height.mas_equalTo(50);
}];
[self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight).offset(-16);
make.centerY.equalTo(self.view);
make.width.height.mas_equalTo(50);
}];
}
#pragma mark - Orchestrator Setup
- (void)setupOrchestrator {
self.orchestrator = [[ConversationOrchestrator alloc] init];
// 配置服务器地址TODO: 替换为实际地址)
// self.orchestrator.asrServerURL = @"wss://your-asr-server.com/ws/asr";
// self.orchestrator.llmServerURL =
// @"https://your-llm-server.com/api/chat/stream";
// self.orchestrator.ttsServerURL = @"https://your-tts-server.com/api/tts";
__weak typeof(self) weakSelf = self;
// 状态变化回调
self.orchestrator.onStateChange = ^(ConversationState state) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf updateStatusForState:state];
};
// 实时识别文本回调
self.orchestrator.onPartialText = ^(NSString *text) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.statusLabel.text = text.length > 0 ? text : @"正在识别...";
};
// 用户最终文本回调
self.orchestrator.onUserFinalText = ^(NSString *text) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
if (text.length > 0) {
[strongSelf.chatView addUserMessage:text];
}
};
// AI 可见文本回调(打字机效果)
self.orchestrator.onAssistantVisibleText = ^(NSString *text) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf.chatView updateLastAssistantMessage:text];
};
// AI 完整回复回调
self.orchestrator.onAssistantFullText = ^(NSString *text) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf.chatView updateLastAssistantMessage:text];
[strongSelf.chatView markLastAssistantMessageComplete];
};
// 音量更新回调
self.orchestrator.onVolumeUpdate = ^(float rms) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf.recordButton updateVolumeRMS:rms];
};
// AI 开始说话
self.orchestrator.onSpeakingStart = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
// 添加空的 AI 消息占位
[strongSelf.chatView addAssistantMessage:@""];
};
// AI 说话结束
self.orchestrator.onSpeakingEnd = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf.chatView markLastAssistantMessageComplete];
};
// 错误回调
self.orchestrator.onError = ^(NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf showError:error];
};
}
#pragma mark - 事件
- (void)showComment {
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
KBAICommentView *customView = [[KBAICommentView alloc]
initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)];
LSTPopView *popView =
[LSTPopView initWithCustomView:customView
parentView:nil
popStyle:LSTPopStyleSmoothFromBottom
dismissStyle:LSTDismissStyleSmoothToBottom];
self.popView = popView;
popView.priority = 1000;
popView.isAvoidKeyboard = false;
popView.hemStyle = LSTHemStyleBottom;
popView.dragStyle = LSTDragStyleY_Positive;
popView.dragDistance = customViewHeight * 0.5;
popView.sweepStyle = LSTSweepStyleY_Positive;
popView.swipeVelocity = 1600;
popView.sweepDismissStyle = LSTSweepDismissStyleSmooth;
[popView pop];
}
- (void)showCommentDirectly {
if (self.commentView.superview) {
[self.view bringSubviewToFront:self.commentView];
return;
}
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
KBAICommentView *customView =
[[KBAICommentView alloc] initWithFrame:CGRectZero];
customView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:customView];
[NSLayoutConstraint activateConstraints:@[
[customView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[customView.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
[customView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[customView.heightAnchor constraintEqualToConstant:customViewHeight],
]];
self.commentView = customView;
}
#pragma mark - UI Updates
- (void)updateStatusForState:(ConversationState)state {
switch (state) {
case ConversationStateIdle:
self.statusLabel.text = @"按住按钮开始对话";
self.recordButton.state = KBAiRecordButtonStateNormal;
break;
case ConversationStateListening:
self.statusLabel.text = @"正在聆听...";
self.recordButton.state = KBAiRecordButtonStateRecording;
break;
case ConversationStateRecognizing:
self.statusLabel.text = @"正在识别...";
self.recordButton.state = KBAiRecordButtonStateNormal;
break;
case ConversationStateThinking:
self.statusLabel.text = @"AI 正在思考...";
self.recordButton.state = KBAiRecordButtonStateNormal;
break;
case ConversationStateSpeaking:
self.statusLabel.text = @"AI 正在回复...";
self.recordButton.state = KBAiRecordButtonStateNormal;
break;
}
}
- (void)showError:(NSError *)error {
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:@"错误"
message:error.localizedDescription
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定"
style:UIAlertActionStyleDefault
handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - KBAiRecordButtonDelegate
- (void)recordButtonDidBeginPress:(KBAiRecordButton *)button {
[self.orchestrator userDidPressRecord];
}
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button {
[self.orchestrator userDidReleaseRecord];
}
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button {
// 取消录音(同样调用 releaseASR 会返回空或部分结果)
[self.orchestrator userDidReleaseRecord];
}
@end