292 lines
8.7 KiB
Mathematica
292 lines
8.7 KiB
Mathematica
|
|
//
|
|||
|
|
// KBAiMainVC.m
|
|||
|
|
// keyBoard
|
|||
|
|
//
|
|||
|
|
// Created by Mac on 2026/1/15.
|
|||
|
|
//
|
|||
|
|
|
|||
|
|
#import "KBAiMainVC.h"
|
|||
|
|
#import "ConversationOrchestrator.h"
|
|||
|
|
#import "KBAiChatView.h"
|
|||
|
|
#import "KBAiRecordButton.h"
|
|||
|
|
#import "KBAICommentView.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) 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];
|
|||
|
|
|
|||
|
|
// 布局约束
|
|||
|
|
[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],
|
|||
|
|
]];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#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];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#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 {
|
|||
|
|
// 取消录音(同样调用 release,ASR 会返回空或部分结果)
|
|||
|
|
[self.orchestrator userDidReleaseRecord];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@end
|