Files
keyboard/keyBoard/Class/AiTalk/VC/KBAiMainVC.m

383 lines
12 KiB
Mathematica
Raw Normal View History

//
// KBAiMainVC.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiMainVC.h"
#import "ConversationOrchestrator.h"
2026-01-16 15:55:08 +08:00
#import "KBAICommentView.h"
#import "KBAiChatView.h"
#import "KBAiRecordButton.h"
#import "LSTPopView.h"
@interface KBAiMainVC () <KBAiRecordButtonDelegate>
2026-01-16 15:55:08 +08:00
@property(nonatomic, weak) LSTPopView *popView;
// UI
@property(nonatomic, strong) KBAiChatView *chatView;
@property(nonatomic, strong) KBAiRecordButton *recordButton;
@property(nonatomic, strong) UILabel *statusLabel;
2026-01-16 15:55:08 +08:00
@property(nonatomic, strong) UIButton *commentButton;
@property(nonatomic, strong) KBAICommentView *commentView;
2026-01-16 21:37:18 +08:00
@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];
2026-01-16 21:37:18 +08:00
//
self.edgesForExtendedLayout = UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars = YES;
[self setupUI];
[self setupOrchestrator];
}
2026-01-19 19:14:20 +08:00
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// TabBar BaseTabBarController
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//
[self.orchestrator stop];
}
2026-01-16 21:37:18 +08:00
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
2026-01-19 19:14:20 +08:00
// mask framemask setupUI
if (self.blurEffectView.layer.mask) {
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
2026-01-16 21:37:18 +08:00
}
}
#pragma mark - UI Setup
- (void)setupUI {
2026-01-19 19:14:20 +08:00
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"AI 助手";
//
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
2026-01-16 21:37:18 +08:00
// 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];
2026-01-19 19:14:20 +08:00
// 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;
2026-01-16 21:37:18 +08:00
//
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];
//
2026-01-16 21:37:18 +08:00
// 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];
2026-01-16 15:55:08 +08:00
//
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];
2026-01-16 21:37:18 +08:00
// - 使 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 -
2026-01-16 15:55:08 +08:00
- (void)showComment {
2026-01-16 21:37:18 +08:00
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];
2026-01-16 15:55:08 +08:00
}
- (void)showCommentDirectly {
if (self.commentView.superview) {
[self.view bringSubviewToFront:self.commentView];
return;
}
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
2026-01-16 21:37:18 +08:00
KBAICommentView *customView =
[[KBAICommentView alloc] initWithFrame:CGRectZero];
2026-01-16 15:55:08 +08:00
customView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:customView];
[NSLayoutConstraint activateConstraints:@[
[customView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
2026-01-16 21:37:18 +08:00
[customView.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
2026-01-16 15:55:08 +08:00
[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