Files
keyboard/keyBoard/Class/AiTalk/VC/KBAiMainVC.m
2026-01-16 15:55:08 +08:00

340 lines
11 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) ConversationOrchestrator *orchestrator;
@end
@implementation KBAiMainVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
[self setupOrchestrator];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 页面消失时停止对话
[self.orchestrator stop];
}
#pragma mark - UI Setup
- (void)setupUI {
self.view.backgroundColor = [UIColor systemBackgroundColor];
self.title = @"AI 助手";
// 安全区域
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
// 状态标签
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];
// 布局约束
[NSLayoutConstraint activateConstraints:@[
// 状态标签
[self.statusLabel.topAnchor constraintEqualToAnchor:safeArea.topAnchor
constant:8],
[self.statusLabel.leadingAnchor
constraintEqualToAnchor:safeArea.leadingAnchor
constant:16],
[self.statusLabel.trailingAnchor
constraintEqualToAnchor:safeArea.trailingAnchor
constant:-16],
// 聊天视图
[self.chatView.topAnchor
constraintEqualToAnchor:self.statusLabel.bottomAnchor
constant:8],
[self.chatView.leadingAnchor
constraintEqualToAnchor:safeArea.leadingAnchor],
[self.chatView.trailingAnchor
constraintEqualToAnchor:safeArea.trailingAnchor],
[self.chatView.bottomAnchor
constraintEqualToAnchor:self.recordButton.topAnchor
constant:-16],
// 录音按钮
[self.recordButton.leadingAnchor
constraintEqualToAnchor:safeArea.leadingAnchor
constant:20],
[self.recordButton.trailingAnchor
constraintEqualToAnchor:safeArea.trailingAnchor
constant:-20],
[self.recordButton.bottomAnchor
constraintEqualToAnchor:safeArea.bottomAnchor
constant:-16],
[self.recordButton.heightAnchor constraintEqualToConstant:50],
// 评论按钮(右侧居中)
[self.commentButton.trailingAnchor
constraintEqualToAnchor:safeArea.trailingAnchor
constant:-16],
[self.commentButton.centerYAnchor
constraintEqualToAnchor:self.chatView.centerYAnchor],
[self.commentButton.widthAnchor constraintEqualToConstant:50],
[self.commentButton.heightAnchor constraintEqualToConstant: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:self.view
popStyle:LSTPopStyleSmoothFromBottom
dismissStyle:LSTDismissStyleSmoothToBottom];
self.popView = popView;
popView.priority = 1000;
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