Files
keyboard/keyBoard/Class/AiTalk/VC/KBAiMainVC.m
2026-01-16 21:37:18 +08:00

405 lines
14 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)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 页面消失时停止对话
[self.orchestrator stop];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 设置黑色渐变层(从底部到顶部,黑色到透明)
if (!self.gradientLayer) {
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.startPoint = CGPointMake(0.53, 1); // 底部
self.gradientLayer.endPoint = CGPointMake(0.54, 0); // 顶部
// 渐变颜色:从黑色 24% 不透明度到透明
self.gradientLayer.colors = @[
(__bridge id)[UIColor colorWithRed:0 / 255.0
green:0 / 255.0
blue:0 / 255.0
alpha:0.24]
.CGColor,
(__bridge id)[UIColor colorWithRed:3 / 255.0
green:3 / 255.0
blue:3 / 255.0
alpha:0]
.CGColor
];
self.gradientLayer.locations = @[ @(0.7), @(1.0) ];
[self.tabbarBackgroundView.layer addSublayer:self.gradientLayer];
}
// 更新黑色渐变层的 frame
self.gradientLayer.frame = self.tabbarBackgroundView.bounds;
// 为 blurEffectView 添加透明度渐变
// mask从底部到中间不透明从中间到顶部透明
if (!self.blurEffectView.layer.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;
}
// 更新 mask 的 frame
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
}
#pragma mark - UI Setup
- (void)setupUI {
self.view.backgroundColor = [UIColor systemBackgroundColor];
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];
// 渐变层将在 viewDidLayoutSubviews 中设置
// 状态标签
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