添加隐私,注销功能

This commit is contained in:
2026-02-27 14:49:46 +08:00
parent a711be4c4d
commit c3e037e070
13 changed files with 247 additions and 846 deletions

View File

@@ -1,17 +0,0 @@
//
// KBAiMainVC.h
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// AI 语音陪伴聊天主界面
@interface KBAiMainVC : BaseViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,809 +0,0 @@
//
// KBAiMainVC.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiMainVC.h"
#import "ConversationOrchestrator.h"
#import "AiVM.h"
#import "AudioSessionManager.h"
#import "DeepgramStreamingManager.h"
#import "KBAICommentView.h"
#import "KBChatTableView.h"
#import "KBAiRecordButton.h"
#import "KBHUD.h"
#import "KBChatLimitPopView.h"
#import "KBPayMainVC.h"
#import "LSTPopView.h"
#import "VoiceChatStreamingManager.h"
#import "KBUserSessionManager.h"
#import <AVFoundation/AVFoundation.h>
@interface KBAiMainVC () <KBAiRecordButtonDelegate,
VoiceChatStreamingManagerDelegate,
DeepgramStreamingManagerDelegate,
AVAudioPlayerDelegate,
KBChatLimitPopViewDelegate>
@property(nonatomic, weak) LSTPopView *popView;
@property(nonatomic, weak) LSTPopView *limitPopView;
// UI
@property(nonatomic, strong) KBChatTableView *chatView;
@property(nonatomic, strong) KBAiRecordButton *recordButton;
@property(nonatomic, strong) UILabel *statusLabel;
@property(nonatomic, strong) UILabel *transcriptLabel;
@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;
@property(nonatomic, strong) VoiceChatStreamingManager *streamingManager;
@property(nonatomic, strong) DeepgramStreamingManager *deepgramManager;
@property(nonatomic, strong) AiVM *aiVM;
@property(nonatomic, strong) AVAudioPlayer *aiAudioPlayer;
@property(nonatomic, strong) NSMutableData *voiceChatAudioBuffer;
//
@property(nonatomic, strong) NSMutableString *assistantVisibleText;
@property(nonatomic, strong) NSMutableString *deepgramFullText;
//
@property(nonatomic, assign) NSTimeInterval lastRMSLogTime;
@end
@implementation KBAiMainVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
//
self.edgesForExtendedLayout = UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars = YES;
[self setupUI];
[self setupOrchestrator];
[self setupStreamingManager];
[self setupDeepgramManager];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// TabBar BaseTabBarController
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//
[self.orchestrator stop];
[self.streamingManager disconnect];
[self.deepgramManager disconnect];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// mask framemask setupUI
if (self.blurEffectView.layer.mask) {
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
}
}
#pragma mark - UI Setup
- (void)setupUI {
self.view.backgroundColor = [UIColor whiteColor];
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];
// 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;
//
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.transcriptLabel = [[UILabel alloc] init];
self.transcriptLabel.text = @"";
self.transcriptLabel.font = [UIFont systemFontOfSize:16];
self.transcriptLabel.textColor = [UIColor labelColor];
self.transcriptLabel.numberOfLines = 0;
self.transcriptLabel.textAlignment = NSTextAlignmentRight;
self.transcriptLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.transcriptLabel];
//
self.chatView = [[KBChatTableView alloc] init];
self.chatView.backgroundColor = [UIColor clearColor];
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);
//
make.height.mas_equalTo(20); //
}];
[self.transcriptLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.statusLabel.mas_bottom).offset(8);
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
//
make.height.mas_equalTo(60); //
}];
//
[self.transcriptLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
[self.chatView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.bottom.equalTo(self.tabbarBackgroundView.mas_top).offset(-8);
make.top.equalTo(self.transcriptLabel.mas_bottom).offset(8);
// 0
make.height.greaterThanOrEqualTo(@100).priority(MASLayoutPriorityDefaultHigh);
}];
// chatView
[self.chatView setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[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];
//
// 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";
__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:@"" audioDuration:0 audioData:nil];
};
// 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 - 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.voiceChatAudioBuffer = [[NSMutableData alloc] init];
self.lastRMSLogTime = 0;
}
#pragma mark - Deepgram Manager
- (void)setupDeepgramManager {
self.deepgramManager = [[DeepgramStreamingManager alloc] init];
self.deepgramManager.delegate = self;
self.deepgramManager.serverURL = @"wss://api.deepgram.com/v1/listen";
self.deepgramManager.apiKey = @"9c792eb63a65d644cbc95785155754cd1e84f8cf";
self.deepgramManager.language = @"en";
self.deepgramManager.model = @"nova-3";
self.deepgramManager.punctuate = YES;
self.deepgramManager.smartFormat = YES;
self.deepgramManager.interimResults = YES;
self.deepgramManager.encoding = @"linear16";
self.deepgramManager.sampleRate = 16000.0;
self.deepgramManager.channels = 1;
[self.deepgramManager prepareConnection];
self.deepgramFullText = [[NSMutableString alloc] init];
self.aiVM = [[AiVM alloc] init];
}
#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 -
- (void)showChatLimitPopWithMessage:(NSString *)message {
if (self.limitPopView) {
[self.limitPopView dismiss];
}
CGFloat width = 252.0;
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0;
KBChatLimitPopView *content =
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
content.message = message;
content.delegate = self;
LSTPopView *popView =
[LSTPopView initWithCustomView:content
parentView:nil
popStyle:LSTPopStyleFade
dismissStyle:LSTDismissStyleFade];
popView.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
popView.hemStyle = LSTHemStyleCenter;
popView.isClickBgDismiss = YES;
popView.isAvoidKeyboard = NO;
self.limitPopView = popView;
[popView pop];
}
#pragma mark - KBChatLimitPopViewDelegate
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
[self.limitPopView dismiss];
}
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
[self.limitPopView dismiss];
if (![KBUserSessionManager shared].isLoggedIn) {
[[KBUserSessionManager shared] goLoginVC];
return;
}
KBPayMainVC *vc = [[KBPayMainVC alloc] init];
vc.initialSelectedIndex = 1; // SVIP
[KB_CURRENT_NAV pushViewController:vc animated:true];
}
#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 {
NSLog(@"[KBAiMainVC] Record button began press");
//
[self.chatView stopPlayingAudio];
NSString *token = [[KBUserSessionManager shared] accessToken] ?: @"";
if (token.length == 0) {
[[KBUserSessionManager shared] goLoginVC];
return;
}
self.statusLabel.text = @"正在连接...";
self.recordButton.state = KBAiRecordButtonStateRecording;
[self.deepgramFullText setString:@""];
self.transcriptLabel.text = @"";
[self.deepgramManager start];
}
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button {
NSLog(@"[KBAiMainVC] Record button end press");
[self.deepgramManager stopAndFinalize];
}
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button {
NSLog(@"[KBAiMainVC] Record button cancel press");
[self.deepgramManager 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 = @"正在识别...";
if (text.length > 0) {
self.transcriptLabel.text = text;
}
}
- (void)voiceChatStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
if (text.length > 0) {
self.transcriptLabel.text = @"";
[self.chatView addUserMessage:text];
}
}
- (void)voiceChatStreamingManagerDidReceiveLLMStart {
self.statusLabel.text = @"AI 正在思考...";
[self.assistantVisibleText setString:@""];
[self.chatView addAssistantMessage:@"" audioDuration:0 audioData:nil];
[self.voiceChatAudioBuffer setLength:0];
}
- (void)voiceChatStreamingManagerDidReceiveLLMToken:(NSString *)token {
if (token.length == 0) {
return;
}
[self.assistantVisibleText appendString:token];
[self.chatView updateLastAssistantMessage:self.assistantVisibleText];
}
- (void)voiceChatStreamingManagerDidReceiveAudioChunk:(NSData *)audioData {
if (audioData.length == 0) {
return;
}
[self.voiceChatAudioBuffer appendData:audioData];
}
- (void)voiceChatStreamingManagerDidCompleteWithTranscript:(NSString *)transcript
aiResponse:(NSString *)aiResponse {
NSString *finalText = aiResponse.length > 0 ? aiResponse : self.assistantVisibleText;
if (aiResponse.length > 0) {
[self.assistantVisibleText setString:aiResponse];
}
//
NSTimeInterval duration = 0;
if (self.voiceChatAudioBuffer.length > 0) {
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:self.voiceChatAudioBuffer
error:&error];
if (!error && player) {
duration = player.duration;
}
}
if (finalText.length > 0) {
[self.chatView updateLastAssistantMessage:finalText];
[self.chatView markLastAssistantMessageComplete];
} else if (transcript.length > 0) {
[self.chatView addAssistantMessage:transcript
audioDuration:duration
audioData:self.voiceChatAudioBuffer.length > 0 ? self.voiceChatAudioBuffer : nil];
}
if (self.voiceChatAudioBuffer.length > 0) {
[self playAiAudioData:self.voiceChatAudioBuffer];
[self.voiceChatAudioBuffer setLength:0];
}
self.recordButton.state = KBAiRecordButtonStateNormal;
self.statusLabel.text = @"完成";
}
- (void)voiceChatStreamingManagerDidFail:(NSError *)error {
self.recordButton.state = KBAiRecordButtonStateNormal;
[self showError:error];
}
#pragma mark - DeepgramStreamingManagerDelegate
- (void)deepgramStreamingManagerDidConnect {
self.statusLabel.text = @"已连接,准备中...";
}
- (void)deepgramStreamingManagerDidDisconnect:(NSError *_Nullable)error {
self.recordButton.state = KBAiRecordButtonStateNormal;
if (error) {
[self showError:error];
}
}
- (void)deepgramStreamingManagerDidUpdateRMS:(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)deepgramStreamingManagerDidReceiveInterimTranscript:(NSString *)text {
self.statusLabel.text = @"正在识别...";
NSString *displayText = text ?: @"";
if (self.deepgramFullText.length > 0 && displayText.length > 0) {
displayText =
[NSString stringWithFormat:@"%@ %@", self.deepgramFullText, displayText];
} else if (self.deepgramFullText.length > 0) {
displayText = [self.deepgramFullText copy];
}
self.transcriptLabel.text = displayText;
}
- (void)deepgramStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
if (text.length > 0) {
if (self.deepgramFullText.length > 0) {
[self.deepgramFullText appendString:@" "];
}
[self.deepgramFullText appendString:text];
}
self.transcriptLabel.text = self.deepgramFullText;
self.statusLabel.text = @"识别完成";
self.recordButton.state = KBAiRecordButtonStateNormal;
NSString *finalText = [self.deepgramFullText copy];
if (finalText.length == 0) {
return;
}
//
[self.chatView addUserMessage:finalText];
__weak typeof(self) weakSelf = self;
[KBHUD showWithStatus:@"AI 思考中..."];
// chat/message
[self.aiVM requestChatMessageWithContent:finalText
companionId:0
completion:^(KBAiMessageResponse *_Nullable response,
NSError *_Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
if (error) {
[KBHUD showError:error.localizedDescription ?: @"请求失败"];
return;
}
if (response.code == 50030) {
NSString *message = response.message ?: @"";
[strongSelf showChatLimitPopWithMessage:message];
return;
}
if (!response || !response.data) {
NSString *message = response.message ?: @"AI 回复为空";
[KBHUD showError:message];
return;
}
// AI
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
if (aiResponse.length == 0) {
[KBHUD showError:@"AI 回复为空"];
return;
}
// audioId
NSString *audioId = response.data.audioId;
// AI audioId
[strongSelf.chatView addAssistantMessage:aiResponse
audioId:audioId];
});
}];
}
- (void)deepgramStreamingManagerDidFail:(NSError *)error {
self.recordButton.state = KBAiRecordButtonStateNormal;
[self showError:error];
}
#pragma mark - Audio Playback
- (void)playAiAudioData:(NSData *)audioData {
if (audioData.length == 0) {
return;
}
NSError *sessionError = nil;
AudioSessionManager *audioSession = [AudioSessionManager sharedManager];
if (![audioSession configureForPlayback:&sessionError]) {
NSLog(@"[KBAiMainVC] Configure playback failed: %@",
sessionError.localizedDescription ?: @"");
}
if (![audioSession activateSession:&sessionError]) {
NSLog(@"[KBAiMainVC] Activate playback session failed: %@",
sessionError.localizedDescription ?: @"");
}
NSError *error = nil;
self.aiAudioPlayer = [[AVAudioPlayer alloc] initWithData:audioData
error:&error];
if (error || !self.aiAudioPlayer) {
NSLog(@"[KBAiMainVC] Audio player init failed: %@",
error.localizedDescription ?: @"");
return;
}
self.aiAudioPlayer.delegate = self;
[self.aiAudioPlayer prepareToPlay];
[self.aiAudioPlayer play];
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player
successfully:(BOOL)flag {
[[AudioSessionManager sharedManager] deactivateSession];
}
@end

View File

@@ -14,6 +14,7 @@
#import "KBChangeNicknamePopView.h"
#import "KBGenderPickerPopView.h"
#import "KBMyVM.h"
#import "KBAlert.h"
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
//
@@ -25,8 +26,10 @@
@property (nonatomic, strong) UIButton *editBadge; //
@property (nonatomic, strong) UILabel *modifyLabel; // Modify
// 退
// 退
@property (nonatomic, strong) UIButton *logoutBtn;
//
@property (nonatomic, strong) UIButton *cancelBtn;
//
@property (nonatomic, copy) NSArray<NSDictionary *> *items; // {title,value,arrow,copy}
@@ -64,9 +67,18 @@
//
self.tableView.tableHeaderView = self.headerView;
// 退
// 退
[self.view addSubview:self.logoutBtn];
[self.logoutBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-(12 + 56 + 10));
make.height.mas_equalTo(56);
}];
//
[self.view addSubview:self.cancelBtn];
[self.cancelBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
@@ -75,7 +87,7 @@
//
UIEdgeInsets inset = self.tableView.contentInset;
inset.bottom = 56 + 24; // +
inset.bottom = 56 + 10 + 56 + 24; // + +
self.tableView.contentInset = inset;
self.viewModel = [[KBMyVM alloc] init];
__weak typeof(self) weakSelf = self;
@@ -275,6 +287,25 @@
[self.myVM logout];
}
- (void)onTapCancelAccount {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_cancel_account_btn"
pageId:@"person_info"
elementId:@"cancel_account_btn"
extra:nil
completion:nil];
KBWeakSelf;
[KBAlert confirmTitle:KBLocalized(@"Cancel Account")
message:KBLocalized(@"After cancellation, your account will be deactivated and local login data will be cleared. Continue?")
ok:KBLocalized(@"Confirm")
cancel:KBLocalized(@"Cancel")
okColor:[UIColor colorWithHex:0xFF0000]
cancelColor:nil
completion:^(BOOL ok) {
if (!ok) { return; }
[weakSelf.myVM cancelAccountWithCompletion:nil];
}];
}
#pragma mark - Lazy UI
- (UITableView *)tableView {
@@ -378,6 +409,19 @@
return _logoutBtn;
}
- (UIButton *)cancelBtn {
if (!_cancelBtn) {
_cancelBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[_cancelBtn setTitle:KBLocalized(@"Cancel Account") forState:UIControlStateNormal];
[_cancelBtn setTitleColor:[UIColor colorWithHex:0xFF0000] forState:UIControlStateNormal];
_cancelBtn.titleLabel.font = [KBFont medium:16];
_cancelBtn.backgroundColor = UIColor.whiteColor;
_cancelBtn.layer.cornerRadius = 12; _cancelBtn.layer.masksToBounds = YES;
[_cancelBtn addTarget:self action:@selector(onTapCancelAccount) forControlEvents:UIControlEventTouchUpInside];
}
return _cancelBtn;
}
#pragma mark - Image Picker
- (void)presentImagePicker {

View File

@@ -31,6 +31,7 @@ typedef void(^KBSubmitFeedbackCompletion)(BOOL success, NSError *_Nullable error
typedef void(^KBMyPurchaseRecordCompletion)(NSArray<KBConsumptionRecord *> *_Nullable records, NSError *_Nullable error);
typedef void(^KBMyInviteCodeCompletion)(KBInviteCodeModel *_Nullable inviteCode, NSError *_Nullable error);
typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSError *_Nullable error);
typedef void(^KBCancelAccountCompletion)(BOOL success, NSError *_Nullable error);
@interface KBMyVM : NSObject
@@ -77,6 +78,9 @@ typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSEr
/// 退出登录
- (void)logout;
/// 注销账号(/user/cancelAccount
- (void)cancelAccountWithCompletion:(KBCancelAccountCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -457,18 +457,67 @@ NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNo
NSString *message = jsonOrData[KBMessage] ?: KBLocalized(@"Success");
[KBHUD showSuccess:message];
// 退
[[KBUserSessionManager shared] logout];
// /
dispatch_async(dispatch_get_main_queue(), ^{
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
AppDelegate *delegate = (AppDelegate *)appDelegate;
[delegate toMainTabbarVC];
}
});
[self kb_clearLoginInfoAndRouteHome];
}];
}
- (void)cancelAccountWithCompletion:(KBCancelAccountCompletion)completion {
[KBHUD show];
[[KBNetworkManager shared] POST:API_USER_CANCEL_ACCOUNT
jsonBody:nil
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary *jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[KBHUD dismiss];
if (error) {
NSString *msg = KBBizMessageFromJSONObject(jsonOrData) ?: error.localizedDescription ?: KBLocalized(@"Network error");
[KBHUD showInfo:msg];
if (completion) {
completion(NO, error);
}
return;
}
NSString *message = jsonOrData[KBMessage] ?: KBLocalized(@"Success");
[KBHUD showSuccess:message];
[self kb_clearLoginInfoAndRouteHome];
if (completion) {
completion(YES, nil);
}
}];
}
#pragma mark - Private
///
- (void)kb_clearLoginInfoAndRouteHome {
[[KBUserSessionManager shared] logout];
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSArray<NSString *> *sharedKeys = @[
AppGroup_MyKbJson,
AppGroup_UserAvatarURL,
AppGroup_SubscriptionPrefillPayload,
AppGroup_ChatUpdatedCompanionId,
@"AppGroup_SelectedPersona"
];
for (NSString *key in sharedKeys) {
[sharedDefaults removeObjectForKey:key];
}
[sharedDefaults synchronize];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults removeObjectForKey:@"KBAISelectedPersonaId"];
[defaults synchronize];
dispatch_async(dispatch_get_main_queue(), ^{
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
AppDelegate *delegate = (AppDelegate *)appDelegate;
[delegate toMainTabbarVC];
}
});
}
@end