2026-01-16 13:38:03 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 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"
|
2026-01-16 13:38:03 +08:00
|
|
|
|
#import "KBAiChatView.h"
|
|
|
|
|
|
#import "KBAiRecordButton.h"
|
|
|
|
|
|
#import "LSTPopView.h"
|
2026-01-21 17:25:38 +08:00
|
|
|
|
#import "VoiceChatStreamingManager.h"
|
|
|
|
|
|
#import "KBUserSessionManager.h"
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
2026-01-21 17:25:38 +08:00
|
|
|
|
@interface KBAiMainVC () <KBAiRecordButtonDelegate,
|
|
|
|
|
|
VoiceChatStreamingManagerDelegate>
|
2026-01-16 15:55:08 +08:00
|
|
|
|
@property(nonatomic, weak) LSTPopView *popView;
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 核心模块
|
|
|
|
|
|
@property(nonatomic, strong) ConversationOrchestrator *orchestrator;
|
2026-01-21 17:25:38 +08:00
|
|
|
|
@property(nonatomic, strong) VoiceChatStreamingManager *streamingManager;
|
|
|
|
|
|
|
|
|
|
|
|
// 文本跟踪
|
|
|
|
|
|
@property(nonatomic, strong) NSMutableString *assistantVisibleText;
|
|
|
|
|
|
|
|
|
|
|
|
// 日志节流
|
|
|
|
|
|
@property(nonatomic, assign) NSTimeInterval lastRMSLogTime;
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBAiMainVC
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
|
|
|
|
|
|
2026-01-16 21:37:18 +08:00
|
|
|
|
// 让视图延伸到屏幕边缘(包括状态栏和导航栏下方)
|
|
|
|
|
|
self.edgesForExtendedLayout = UIRectEdgeAll;
|
|
|
|
|
|
self.extendedLayoutIncludesOpaqueBars = YES;
|
|
|
|
|
|
|
2026-01-16 13:38:03 +08:00
|
|
|
|
[self setupUI];
|
|
|
|
|
|
[self setupOrchestrator];
|
2026-01-21 17:25:38 +08:00
|
|
|
|
[self setupStreamingManager];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 19:14:20 +08:00
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
|
|
|
|
[super viewWillAppear:animated];
|
|
|
|
|
|
// TabBar 背景色由 BaseTabBarController 统一管理,这里不需要设置
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 13:38:03 +08:00
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
|
|
|
|
[super viewWillDisappear:animated];
|
|
|
|
|
|
|
|
|
|
|
|
// 页面消失时停止对话
|
|
|
|
|
|
[self.orchestrator stop];
|
2026-01-21 17:25:38 +08:00
|
|
|
|
[self.streamingManager disconnect];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:37:18 +08:00
|
|
|
|
- (void)viewDidLayoutSubviews {
|
|
|
|
|
|
[super viewDidLayoutSubviews];
|
|
|
|
|
|
|
2026-01-19 19:14:20 +08:00
|
|
|
|
// 只更新 mask 的 frame(mask 已在 setupUI 中创建)
|
|
|
|
|
|
if (self.blurEffectView.layer.mask) {
|
|
|
|
|
|
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
|
2026-01-16 21:37:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 13:38:03 +08:00
|
|
|
|
#pragma mark - UI Setup
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
2026-01-19 19:14:20 +08:00
|
|
|
|
self.view.backgroundColor = [UIColor whiteColor];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
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
|
|
|
|
|
2026-01-16 13:38:03 +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];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 录音按钮
|
|
|
|
|
|
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);
|
|
|
|
|
|
}];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Orchestrator Setup
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupOrchestrator {
|
|
|
|
|
|
self.orchestrator = [[ConversationOrchestrator alloc] init];
|
|
|
|
|
|
|
2026-01-21 17:25:38 +08:00
|
|
|
|
// 配置服务器地址
|
|
|
|
|
|
// 1. ASR 语音识别服务(WebSocket)
|
|
|
|
|
|
self.orchestrator.asrServerURL = @"ws://192.168.2.21:7529/ws/asr";
|
|
|
|
|
|
|
|
|
|
|
|
// 2. LLM 大语言模型服务(HTTP Stream)
|
|
|
|
|
|
self.orchestrator.llmServerURL = @"http://192.168.2.21:7529/api/chat/stream";
|
|
|
|
|
|
|
|
|
|
|
|
// 3. TTS 语音合成服务(HTTP)
|
|
|
|
|
|
self.orchestrator.ttsServerURL = @"http://192.168.2.21:7529/api/tts/stream";
|
2026-01-16 13:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
__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];
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 17:25:38 +08:00
|
|
|
|
#pragma mark - Streaming Manager
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupStreamingManager {
|
|
|
|
|
|
self.streamingManager = [[VoiceChatStreamingManager alloc] init];
|
|
|
|
|
|
self.streamingManager.delegate = self;
|
|
|
|
|
|
self.streamingManager.serverURL = @"ws://192.168.2.21:7529/api/ws/chat";
|
|
|
|
|
|
self.assistantVisibleText = [[NSMutableString alloc] init];
|
|
|
|
|
|
self.lastRMSLogTime = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 13:38:03 +08:00
|
|
|
|
#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;
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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 {
|
2026-01-21 17:25:38 +08:00
|
|
|
|
NSLog(@"[KBAiMainVC] Record button began press");
|
|
|
|
|
|
NSString *token = [[KBUserSessionManager shared] accessToken] ?: @"";
|
|
|
|
|
|
if (token.length == 0) {
|
|
|
|
|
|
[[KBUserSessionManager shared] goLoginVC];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.statusLabel.text = @"正在连接...";
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
|
|
|
|
[self.streamingManager startWithToken:token language:@"en" voiceId:nil];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button {
|
2026-01-21 17:25:38 +08:00
|
|
|
|
NSLog(@"[KBAiMainVC] Record button end press");
|
|
|
|
|
|
[self.streamingManager stopAndFinalize];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button {
|
2026-01-21 17:25:38 +08:00
|
|
|
|
NSLog(@"[KBAiMainVC] Record button cancel press");
|
|
|
|
|
|
[self.streamingManager cancel];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - VoiceChatStreamingManagerDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidConnect {
|
|
|
|
|
|
self.statusLabel.text = @"已连接,准备中...";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidDisconnect:(NSError *_Nullable)error {
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
[self showError:error];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidStartSession:(NSString *)sessionId {
|
|
|
|
|
|
self.statusLabel.text = @"正在聆听...";
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidStartTurn:(NSInteger)turnIndex {
|
|
|
|
|
|
self.statusLabel.text = @"正在聆听...";
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveEagerEndOfTurnWithTranscript:(NSString *)text
|
|
|
|
|
|
confidence:(double)confidence {
|
|
|
|
|
|
self.statusLabel.text = @"准备响应...";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidResumeTurn {
|
|
|
|
|
|
self.statusLabel.text = @"正在聆听...";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidUpdateRMS:(float)rms {
|
|
|
|
|
|
[self.recordButton updateVolumeRMS:rms];
|
|
|
|
|
|
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
|
|
|
|
if (now - self.lastRMSLogTime >= 1.0) {
|
|
|
|
|
|
self.lastRMSLogTime = now;
|
|
|
|
|
|
NSLog(@"[KBAiMainVC] RMS: %.3f", rms);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveInterimTranscript:(NSString *)text {
|
|
|
|
|
|
self.statusLabel.text = text.length > 0 ? text : @"正在识别...";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
|
|
|
|
|
|
if (text.length > 0) {
|
|
|
|
|
|
[self.chatView addUserMessage:text];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveLLMStart {
|
|
|
|
|
|
self.statusLabel.text = @"AI 正在思考...";
|
|
|
|
|
|
[self.assistantVisibleText setString:@""];
|
|
|
|
|
|
[self.chatView addAssistantMessage:@""];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveLLMToken:(NSString *)token {
|
|
|
|
|
|
if (token.length == 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[self.assistantVisibleText appendString:token];
|
|
|
|
|
|
[self.chatView updateLastAssistantMessage:self.assistantVisibleText];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidReceiveAudioChunk:(NSData *)audioData {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidCompleteWithTranscript:(NSString *)transcript
|
|
|
|
|
|
aiResponse:(NSString *)aiResponse {
|
|
|
|
|
|
NSString *finalText = aiResponse.length > 0 ? aiResponse
|
|
|
|
|
|
: self.assistantVisibleText;
|
|
|
|
|
|
if (finalText.length > 0) {
|
|
|
|
|
|
[self.chatView updateLastAssistantMessage:finalText];
|
|
|
|
|
|
[self.chatView markLastAssistantMessageComplete];
|
|
|
|
|
|
}
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
|
|
|
|
self.statusLabel.text = @"完成";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)voiceChatStreamingManagerDidFail:(NSError *)error {
|
|
|
|
|
|
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
|
|
|
|
[self showError:error];
|
2026-01-16 13:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|