10 Commits

Author SHA1 Message Date
fa9af5ff1b 处理聊天信息没回来 切刀下一个collectionviewcell ,信息不对的问题(禁用滚动) 2026-02-03 14:05:29 +08:00
08628bcd1d aihome里统一发布消息控件 2026-02-03 13:31:52 +08:00
19cb29616f 优化发送输入框 2026-02-02 21:25:28 +08:00
6e50cdcd2a 1 2026-02-02 20:36:38 +08:00
f1b52151be 添加音频动画 2026-02-02 19:49:56 +08:00
993ec623af 1 2026-02-02 19:07:00 +08:00
0416a64235 处理为识别到语音 2026-02-02 17:41:23 +08:00
2b75ad90fb 修改举报和UI 2026-02-02 17:07:46 +08:00
0ac9030f80 1 2026-02-02 15:28:00 +08:00
ea9c40f64f 处理语音在B界面删除历史记录有声音问题 2026-02-02 15:27:55 +08:00
27 changed files with 845 additions and 246 deletions

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ai_close_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ai_close_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ai_report_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ai_report_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ai_sendmessage_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ai_sendmessage_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -60,6 +60,17 @@ NS_ASSUME_NONNULL_BEGIN
/// 移除 loading AI 消息 /// 移除 loading AI 消息
- (void)removeLoadingAssistantMessage; - (void)removeLoadingAssistantMessage;
/// 移除 loading 用户消息
- (void)removeLoadingUserMessage;
/// 顶部加载中提示
- (void)showTopLoading;
- (void)hideTopLoading;
/// 顶部“无更多数据”提示
- (void)showNoMoreData;
- (void)hideNoMoreData;
/// 滚动到底部 /// 滚动到底部
- (void)scrollToBottom; - (void)scrollToBottom;

View File

@@ -37,6 +37,11 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
@property (nonatomic, assign) CGSize lastIntroFooterTableSize; @property (nonatomic, assign) CGSize lastIntroFooterTableSize;
@property (nonatomic, assign) BOOL applyingIntroFooter; @property (nonatomic, assign) BOOL applyingIntroFooter;
@property (nonatomic, copy) NSString *remoteAudioToken; @property (nonatomic, copy) NSString *remoteAudioToken;
@property (nonatomic, strong) UIView *topStatusView;
@property (nonatomic, strong) UIActivityIndicatorView *topLoadingIndicator;
@property (nonatomic, strong) UILabel *topStatusLabel;
@property (nonatomic, assign) BOOL isTopLoading;
@property (nonatomic, assign) BOOL isTopNoMore;
@end @end
@@ -401,6 +406,115 @@ static inline CGFloat KBChatAbsTimeInterval(NSTimeInterval interval) {
} }
} }
- (void)removeLoadingUserMessage {
if (self.inverted) {
for (NSInteger i = 0; i < self.messages.count; i++) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
[self.messages removeObjectAtIndex:i];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
NSLog(@"[KBChatTableView] 移除 loading 用户消息,索引: %ld", (long)i);
break;
}
}
} else {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
[self.messages removeObjectAtIndex:i];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
NSLog(@"[KBChatTableView] 移除 loading 用户消息,索引: %ld", (long)i);
break;
}
}
}
}
#pragma mark - Top Status
- (void)showTopLoading {
self.isTopLoading = YES;
self.isTopNoMore = NO;
[self updateTopStatusView];
}
- (void)hideTopLoading {
self.isTopLoading = NO;
[self updateTopStatusView];
}
- (void)showNoMoreData {
self.isTopNoMore = YES;
self.isTopLoading = NO;
[self updateTopStatusView];
}
- (void)hideNoMoreData {
self.isTopNoMore = NO;
[self updateTopStatusView];
}
- (void)updateTopStatusView {
BOOL shouldShow = self.isTopLoading || self.isTopNoMore;
if (!shouldShow) {
self.topStatusView.hidden = YES;
return;
}
if (!self.topStatusView) {
self.topStatusView = [[UIView alloc] initWithFrame:CGRectZero];
self.topStatusView.backgroundColor = [UIColor clearColor];
self.topStatusView.userInteractionEnabled = NO;
self.topLoadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
self.topLoadingIndicator.hidesWhenStopped = YES;
[self.topStatusView addSubview:self.topLoadingIndicator];
self.topStatusLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.topStatusLabel.font = [UIFont systemFontOfSize:12];
self.topStatusLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
[self.topStatusView addSubview:self.topStatusLabel];
[self addSubview:self.topStatusView];
}
if (self.isTopLoading) {
self.topStatusLabel.text = KBLocalized(@"Loading...");
[self.topLoadingIndicator startAnimating];
} else if (self.isTopNoMore) {
self.topStatusLabel.text = KBLocalized(@"No more data");
[self.topLoadingIndicator stopAnimating];
}
CGFloat width = CGRectGetWidth(self.tableView.bounds);
if (width <= 0) {
width = CGRectGetWidth(self.bounds);
}
CGFloat height = 32;
self.topStatusView.frame = CGRectMake(0, 0, width, height);
self.topStatusView.hidden = NO;
[self bringSubviewToFront:self.topStatusView];
CGSize labelSize = [self.topStatusLabel sizeThatFits:CGSizeMake(width - 40, height)];
CGFloat totalWidth = labelSize.width + (self.isTopLoading ? 20 + 6 : 0);
CGFloat startX = (width - totalWidth) / 2.0;
if (self.isTopLoading) {
self.topLoadingIndicator.frame = CGRectMake(startX, (height - 20) / 2.0, 20, 20);
self.topStatusLabel.frame = CGRectMake(CGRectGetMaxX(self.topLoadingIndicator.frame) + 6,
(height - labelSize.height) / 2.0,
labelSize.width,
labelSize.height);
} else {
self.topStatusLabel.frame = CGRectMake((width - labelSize.width) / 2.0,
(height - labelSize.height) / 2.0,
labelSize.width,
labelSize.height);
}
}
- (void)scrollToBottom { - (void)scrollToBottom {
[self scrollToBottomAnimated:YES]; [self scrollToBottomAnimated:YES];
} }

View File

@@ -36,6 +36,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 更新最后一条用户消息 /// 更新最后一条用户消息
- (void)updateLastUserMessage:(NSString *)text; - (void)updateLastUserMessage:(NSString *)text;
/// 移除 loading 用户消息
- (void)removeLoadingUserMessage;
/// 添加 AI 消息(支持打字机效果) /// 添加 AI 消息(支持打字机效果)
- (void)appendAssistantMessage:(NSString *)text - (void)appendAssistantMessage:(NSString *)text
audioId:(nullable NSString *)audioId; audioId:(nullable NSString *)audioId;

View File

@@ -30,11 +30,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
/// ///
@property (nonatomic, strong) UILabel *nameLabel; @property (nonatomic, strong) UILabel *nameLabel;
///
@property (nonatomic, strong) UILabel *openingLabel;
/// ///
@property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages; @property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages;
@@ -69,6 +64,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
@property (nonatomic, assign) BOOL isCurrentPersonaCell; @property (nonatomic, assign) BOOL isCurrentPersonaCell;
@property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio; @property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio;
@property (nonatomic, assign) BOOL hasPlayedPrologueAudio; @property (nonatomic, assign) BOOL hasPlayedPrologueAudio;
@property (nonatomic, assign) BOOL shouldShowOpeningMessage;
@end @end
@@ -107,6 +103,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
self.isCurrentPersonaCell = NO; self.isCurrentPersonaCell = NO;
self.shouldAutoPlayPrologueAudio = NO; self.shouldAutoPlayPrologueAudio = NO;
self.hasPlayedPrologueAudio = NO; self.hasPlayedPrologueAudio = NO;
self.shouldShowOpeningMessage = NO;
self.shouldShowOpeningMessage = NO;
// self.hasLoadedData = NO; // self.hasLoadedData = NO;
// Cell // Cell
@@ -129,14 +127,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
make.edges.equalTo(self.contentView); make.edges.equalTo(self.contentView);
}]; }];
//
[self.contentView addSubview:self.openingLabel];
[self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT);
make.left.equalTo(self.contentView).offset(40);
make.right.equalTo(self.contentView).offset(-40);
}];
// //
[self.contentView addSubview:self.avatarImageView]; [self.contentView addSubview:self.avatarImageView];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -172,8 +162,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
// //
[self.contentView addSubview:self.chatView]; [self.contentView addSubview:self.chatView];
CGFloat topY = KB_STATUSBAR_HEIGHT + 15;
[self.chatView mas_makeConstraints:^(MASConstraintMaker *make) { [self.chatView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT); make.top.equalTo(self.contentView).offset(topY);
make.left.right.equalTo(self.contentView); make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.avatarImageView.mas_top).offset(-10); make.bottom.equalTo(self.avatarImageView.mas_top).offset(-10);
}]; }];
@@ -220,8 +211,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl] [self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]]; placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
self.nameLabel.text = persona.name; self.nameLabel.text = persona.name;
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.prologue;
// //
[self.chatView stopPlayingAudio]; [self.chatView stopPlayingAudio];
@@ -232,6 +221,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
NSLog(@"[KBPersonaChatCell] contentView.frame: %@", NSStringFromCGRect(self.contentView.frame)); NSLog(@"[KBPersonaChatCell] contentView.frame: %@", NSStringFromCGRect(self.contentView.frame));
if (self.messages.count > 0) { if (self.messages.count > 0) {
self.shouldShowOpeningMessage = NO;
[self removeOpeningMessageIfNeeded];
[self.chatView updateIntroFooterText:nil]; [self.chatView updateIntroFooterText:nil];
[self ensureOpeningMessageAtTop]; [self ensureOpeningMessageAtTop];
// //
@@ -241,8 +232,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
keepOffset:NO keepOffset:NO
scrollToBottom:YES]; scrollToBottom:YES];
} else { } else {
self.shouldShowOpeningMessage = NO;
[self.chatView clearMessages]; [self.chatView clearMessages];
[self.chatView updateIntroFooterText:persona.prologue]; [self.chatView updateIntroFooterText:nil];
} }
NSLog(@"[KBPersonaChatCell] ========== setPersona 结束 =========="); NSLog(@"[KBPersonaChatCell] ========== setPersona 结束 ==========");
@@ -269,6 +261,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
self.isLoading = YES; self.isLoading = YES;
BOOL isLoadMore = (self.currentPage > 1);
if (isLoadMore) {
[self.chatView showTopLoading];
}
if (self.currentPage == 1) { if (self.currentPage == 1) {
[self.chatView resetNoMoreData]; [self.chatView resetNoMoreData];
@@ -291,8 +287,12 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
strongSelf.isLoading = NO; strongSelf.isLoading = NO;
if (isLoadMore) {
[strongSelf.chatView hideTopLoading];
}
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
if (strongSelf.currentPage == 1 && strongSelf.persona.prologue.length > 0) { if (strongSelf.currentPage == 1 && strongSelf.persona.prologue.length > 0) {
strongSelf.shouldShowOpeningMessage = YES;
[strongSelf showOpeningMessage]; [strongSelf showOpeningMessage];
} }
}); });
@@ -306,6 +306,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
if (loadedPage == 1) { if (loadedPage == 1) {
BOOL isEmpty = (pageModel.total == 0); BOOL isEmpty = (pageModel.total == 0);
strongSelf.shouldAutoPlayPrologueAudio = isEmpty && (strongSelf.persona.prologueAudio.length > 0); strongSelf.shouldAutoPlayPrologueAudio = isEmpty && (strongSelf.persona.prologueAudio.length > 0);
strongSelf.shouldShowOpeningMessage = isEmpty;
if (!strongSelf.shouldAutoPlayPrologueAudio) { if (!strongSelf.shouldAutoPlayPrologueAudio) {
[strongSelf.chatView stopPlayingAudio]; [strongSelf.chatView stopPlayingAudio];
} else { } else {
@@ -317,6 +318,8 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
[strongSelf.chatView clearMessages]; [strongSelf.chatView clearMessages];
[strongSelf.chatView updateIntroFooterText:strongSelf.persona.prologue]; [strongSelf.chatView updateIntroFooterText:strongSelf.persona.prologue];
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
[strongSelf.chatView hideTopLoading];
[strongSelf.chatView hideNoMoreData];
strongSelf.isLoading = NO; strongSelf.isLoading = NO;
}); });
strongSelf.currentPage++; strongSelf.currentPage++;
@@ -355,6 +358,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
if (loadedPage == 1) { if (loadedPage == 1) {
// //
strongSelf.messages = newMessages; strongSelf.messages = newMessages;
if (!strongSelf.shouldShowOpeningMessage) {
[strongSelf removeOpeningMessageIfNeeded];
}
[strongSelf ensureOpeningMessageAtTop]; [strongSelf ensureOpeningMessageAtTop];
} else { } else {
// //
@@ -375,6 +381,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
// UI // UI
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (isLoadMore) {
[strongSelf.chatView hideTopLoading];
}
if (loadedPage == 1) { if (loadedPage == 1) {
NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: 0, scrollToBottom: 1", NSLog(@"[KBPersonaChatCell] 刷新 UI - loadedPage: %ld, keepOffset: 0, scrollToBottom: 1",
(long)loadedPage); (long)loadedPage);
@@ -394,6 +403,11 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
} }
[strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory]; [strongSelf.chatView endLoadMoreWithHasMoreData:strongSelf.hasMoreHistory];
if (!strongSelf.hasMoreHistory && strongSelf.messages.count > 0) {
[strongSelf.chatView showNoMoreData];
} else {
[strongSelf.chatView hideNoMoreData];
}
// //
[[KBAIChatMessageCacheManager shared] saveMessages:strongSelf.messages [[KBAIChatMessageCacheManager shared] saveMessages:strongSelf.messages
@@ -449,6 +463,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
- (void)showOpeningMessage { - (void)showOpeningMessage {
if (!self.shouldShowOpeningMessage) {
return;
}
if (self.messages.count == 0) { if (self.messages.count == 0) {
[self.chatView clearMessages]; [self.chatView clearMessages];
[self.chatView updateIntroFooterText:self.persona.prologue]; [self.chatView updateIntroFooterText:self.persona.prologue];
@@ -487,6 +504,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
- (void)ensureOpeningMessageAtTop { - (void)ensureOpeningMessageAtTop {
if (!self.shouldShowOpeningMessage) {
return;
}
NSString *prologue = [self currentPrologueText]; NSString *prologue = [self currentPrologueText];
if (prologue.length == 0) { if (prologue.length == 0) {
return; return;
@@ -507,6 +527,14 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
} }
- (void)removeOpeningMessageIfNeeded {
NSInteger index = [self openingMessageIndexInMessages];
if (index == NSNotFound) {
return;
}
[self.messages removeObjectAtIndex:index];
}
- (nullable KBAiChatMessage *)openingMessageInMessages { - (nullable KBAiChatMessage *)openingMessageInMessages {
NSInteger index = [self openingMessageIndexInMessages]; NSInteger index = [self openingMessageIndexInMessages];
if (index == NSNotFound) { if (index == NSNotFound) {
@@ -546,6 +574,11 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
if (self.persona && self.persona.personaId == companionId) { if (self.persona && self.persona.personaId == companionId) {
NSLog(@"[KBPersonaChatCell] 收到聊天重置通知companionId=%ld, 清空聊天记录", (long)companionId); NSLog(@"[KBPersonaChatCell] 收到聊天重置通知companionId=%ld, 清空聊天记录", (long)companionId);
self.shouldAutoPlayPrologueAudio = NO;
self.hasPlayedPrologueAudio = NO;
self.shouldShowOpeningMessage = YES;
[self.chatView stopPlayingAudio];
// //
self.messages = [NSMutableArray array]; self.messages = [NSMutableArray array];
self.hasLoadedData = NO; self.hasLoadedData = NO;
@@ -633,6 +666,32 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
} }
} }
- (void)removeLoadingUserMessage {
if (!self.messages) {
return;
}
if (self.chatView.inverted) {
for (NSInteger i = 0; i < self.messages.count; i++) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
[self.messages removeObjectAtIndex:i];
break;
}
}
} else {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
[self.messages removeObjectAtIndex:i];
break;
}
}
}
[self.chatView removeLoadingUserMessage];
}
- (void)markLastUserMessageLoadingComplete { - (void)markLastUserMessageLoadingComplete {
[self.chatView markLastUserMessageLoadingComplete]; [self.chatView markLastUserMessageLoadingComplete];
@@ -877,17 +936,6 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
return _nameLabel; return _nameLabel;
} }
- (UILabel *)openingLabel {
if (!_openingLabel) {
_openingLabel = [[UILabel alloc] init];
_openingLabel.font = [UIFont systemFontOfSize:14];
_openingLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
_openingLabel.textAlignment = NSTextAlignmentCenter;
_openingLabel.numberOfLines = 2;
}
return _openingLabel;
}
- (KBChatTableView *)chatView { - (KBChatTableView *)chatView {
if (!_chatView) { if (!_chatView) {
_chatView = [[KBChatTableView alloc] init]; _chatView = [[KBChatTableView alloc] init];
@@ -1027,7 +1075,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
[self.popView dismiss]; [self.popView dismiss];
} }
CGFloat customViewHeight = KB_SCREEN_HEIGHT * 0.7; CGFloat customViewHeight = KB_SCREEN_HEIGHT * 0.75;
KBAICommentView *customView = [[KBAICommentView alloc] KBAICommentView *customView = [[KBAICommentView alloc]
initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)];

View File

@@ -103,7 +103,7 @@
if (!_actionButton) { if (!_actionButton) {
_actionButton = [UIButton buttonWithType:UIButtonTypeCustom]; _actionButton = [UIButton buttonWithType:UIButtonTypeCustom];
_actionButton.titleLabel.font = [UIFont systemFontOfSize:13]; _actionButton.titleLabel.font = [UIFont systemFontOfSize:13];
[_actionButton setTitleColor:[UIColor secondaryLabelColor] [_actionButton setTitleColor:[UIColor whiteColor]
forState:UIControlStateNormal]; forState:UIControlStateNormal];
_actionButton.tintColor = [UIColor secondaryLabelColor]; _actionButton.tintColor = [UIColor secondaryLabelColor];

View File

@@ -11,8 +11,9 @@
@interface KBAICommentInputView () <UITextFieldDelegate> @interface KBAICommentInputView () <UITextFieldDelegate>
@property(nonatomic, strong) UIView *containerView; @property(nonatomic, strong) UIView *containerView;
@property(nonatomic, strong) UIImageView *avatarImageView; //@property(nonatomic, strong) UIImageView *avatarImageView;
@property(nonatomic, strong) UITextField *textField; @property(nonatomic, strong) UITextField *textField;
@property(nonatomic, strong) UILabel *placeholderLabel;
@property(nonatomic, strong) UIButton *sendButton; @property(nonatomic, strong) UIButton *sendButton;
@property(nonatomic, strong) UIView *topLine; @property(nonatomic, strong) UIView *topLine;
@@ -31,12 +32,13 @@
#pragma mark - UI Setup #pragma mark - UI Setup
- (void)setupUI { - (void)setupUI {
self.backgroundColor = [UIColor whiteColor]; self.backgroundColor = [UIColor colorWithHex:0x797979 alpha:0.49];
[self addSubview:self.topLine]; [self addSubview:self.topLine];
[self addSubview:self.avatarImageView]; // [self addSubview:self.avatarImageView];
[self addSubview:self.containerView]; [self addSubview:self.containerView];
[self.containerView addSubview:self.textField]; [self.containerView addSubview:self.textField];
[self addSubview:self.placeholderLabel];
[self addSubview:self.sendButton]; [self addSubview:self.sendButton];
[self.topLine mas_makeConstraints:^(MASConstraintMaker *make) { [self.topLine mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -44,17 +46,17 @@
make.height.mas_equalTo(0.5); make.height.mas_equalTo(0.5);
}]; }];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { // [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self).offset(12); // make.left.equalTo(self).offset(12);
make.centerY.equalTo(self); // make.centerY.equalTo(self);
make.width.height.mas_equalTo(32); // make.width.height.mas_equalTo(32);
}]; // }];
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.avatarImageView.mas_right).offset(10); make.left.equalTo(self).offset(12);
make.right.equalTo(self.sendButton.mas_left).offset(-10); make.right.equalTo(self.sendButton.mas_left).offset(-12);
make.centerY.equalTo(self); make.centerY.equalTo(self);
make.height.mas_equalTo(36); make.height.mas_equalTo(52);
}]; }];
[self.textField mas_makeConstraints:^(MASConstraintMaker *make) { [self.textField mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -69,17 +71,23 @@
make.width.mas_equalTo(50); make.width.mas_equalTo(50);
make.height.mas_equalTo(30); make.height.mas_equalTo(30);
}]; }];
[self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
}];
} }
#pragma mark - Public Methods #pragma mark - Public Methods
- (void)setPlaceholder:(NSString *)placeholder { - (void)setPlaceholder:(NSString *)placeholder {
_placeholder = placeholder; _placeholder = placeholder;
self.textField.placeholder = placeholder; self.placeholderLabel.text = placeholder;
[self updatePlaceholderVisibility];
} }
- (void)clearText { - (void)clearText {
self.textField.text = @""; self.textField.text = @"";
[self updatePlaceholderVisibility];
[self updateSendButtonState]; [self updateSendButtonState];
} }
@@ -97,13 +105,14 @@
} }
- (void)textFieldDidChange:(UITextField *)textField { - (void)textFieldDidChange:(UITextField *)textField {
[self updatePlaceholderVisibility];
[self updateSendButtonState]; [self updateSendButtonState];
} }
- (void)updateSendButtonState { - (void)updateSendButtonState {
BOOL hasText = self.textField.text.length > 0; BOOL hasText = self.textField.text.length > 0;
self.sendButton.enabled = hasText; self.sendButton.enabled = hasText;
self.sendButton.alpha = hasText ? 1.0 : 0.5; // self.sendButton.alpha = hasText ? 1.0 : 0.5;
} }
#pragma mark - UITextFieldDelegate #pragma mark - UITextFieldDelegate
@@ -113,6 +122,15 @@
return YES; return YES;
} }
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
[self updatePlaceholderVisibility];
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
[self updatePlaceholderVisibility];
}
#pragma mark - Lazy Loading #pragma mark - Lazy Loading
- (UIView *)topLine { - (UIView *)topLine {
@@ -123,25 +141,24 @@
return _topLine; return _topLine;
} }
- (UIImageView *)avatarImageView { //- (UIImageView *)avatarImageView {
if (!_avatarImageView) { // if (!_avatarImageView) {
_avatarImageView = [[UIImageView alloc] init]; // _avatarImageView = [[UIImageView alloc] init];
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill; // _avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
_avatarImageView.layer.cornerRadius = 16; // _avatarImageView.layer.cornerRadius = 16;
_avatarImageView.layer.masksToBounds = YES; // _avatarImageView.layer.masksToBounds = YES;
_avatarImageView.backgroundColor = [UIColor systemGray5Color]; // _avatarImageView.backgroundColor = [UIColor systemGray5Color];
_avatarImageView.image = [UIImage systemImageNamed:@"person.circle.fill"]; // _avatarImageView.image = [UIImage systemImageNamed:@"person.circle.fill"];
_avatarImageView.tintColor = [UIColor systemGray3Color]; // _avatarImageView.tintColor = [UIColor systemGray3Color];
} // }
return _avatarImageView; // return _avatarImageView;
} //}
- (UIView *)containerView { - (UIView *)containerView {
if (!_containerView) { if (!_containerView) {
_containerView = [[UIView alloc] init]; _containerView = [[UIView alloc] init];
_containerView.backgroundColor = [UIColor systemGray6Color]; // _containerView.layer.cornerRadius = 26;
_containerView.layer.cornerRadius = 18; // _containerView.layer.masksToBounds = YES;
_containerView.layer.masksToBounds = YES;
} }
return _containerView; return _containerView;
} }
@@ -149,27 +166,42 @@
- (UITextField *)textField { - (UITextField *)textField {
if (!_textField) { if (!_textField) {
_textField = [[UITextField alloc] init]; _textField = [[UITextField alloc] init];
_textField.placeholder = @"说点什么..."; _textField.textColor = [UIColor whiteColor];
_textField.textAlignment = NSTextAlignmentLeft;
_textField.font = [UIFont systemFontOfSize:14]; _textField.font = [UIFont systemFontOfSize:14];
_textField.delegate = self; _textField.delegate = self;
_textField.returnKeyType = UIReturnKeySend; _textField.returnKeyType = UIReturnKeySend;
[_textField addTarget:self [_textField addTarget:self
action:@selector(textFieldDidChange:) action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged]; forControlEvents:UIControlEventEditingChanged];
[self updatePlaceholderVisibility];
} }
return _textField; return _textField;
} }
- (UILabel *)placeholderLabel {
if (!_placeholderLabel) {
_placeholderLabel = [[UILabel alloc] init];
_placeholderLabel.text = @"Send A Message";
_placeholderLabel.textColor = [UIColor whiteColor];
_placeholderLabel.font = [UIFont systemFontOfSize:14];
_placeholderLabel.textAlignment = NSTextAlignmentCenter;
_placeholderLabel.userInteractionEnabled = NO;
}
return _placeholderLabel;
}
- (UIButton *)sendButton { - (UIButton *)sendButton {
if (!_sendButton) { if (!_sendButton) {
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom]; _sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_sendButton setTitle:@"发送" forState:UIControlStateNormal]; // [_sendButton setTitle:@"发送" forState:UIControlStateNormal];
[_sendButton setTitleColor:[UIColor systemBlueColor] // [_sendButton setTitleColor:[UIColor systemBlueColor]
forState:UIControlStateNormal]; // forState:UIControlStateNormal];
_sendButton.titleLabel.font = [UIFont systemFontOfSize:15 // _sendButton.titleLabel.font = [UIFont systemFontOfSize:15
weight:UIFontWeightMedium]; // weight:UIFontWeightMedium];
[_sendButton setImage:[UIImage imageNamed:@"ai_sendmessage_icon"] forState:UIControlStateNormal];
_sendButton.enabled = NO; _sendButton.enabled = NO;
_sendButton.alpha = 0.5; // _sendButton.alpha = 0.5;
[_sendButton addTarget:self [_sendButton addTarget:self
action:@selector(sendButtonTapped) action:@selector(sendButtonTapped)
forControlEvents:UIControlEventTouchUpInside]; forControlEvents:UIControlEventTouchUpInside];
@@ -177,4 +209,11 @@
return _sendButton; return _sendButton;
} }
#pragma mark - Private
- (void)updatePlaceholderVisibility {
BOOL hasText = self.textField.text.length > 0;
self.placeholderLabel.hidden = hasText;
}
@end @end

View File

@@ -189,8 +189,8 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
}]; }];
[self.inputView mas_makeConstraints:^(MASConstraintMaker *make) { [self.inputView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self); make.left.right.equalTo(self).inset(12);
make.height.mas_equalTo(50); make.height.mas_equalTo(52);
self.inputBottomConstraint = self.inputBottomConstraint =
make.bottom.equalTo(self).offset(-KB_SafeAreaBottom()); make.bottom.equalTo(self).offset(-KB_SafeAreaBottom());
}]; }];
@@ -846,8 +846,9 @@ static NSInteger const kRepliesLoadCount = 5;
- (KBAICommentInputView *)inputView { - (KBAICommentInputView *)inputView {
if (!_inputView) { if (!_inputView) {
_inputView = [[KBAICommentInputView alloc] init]; _inputView = [[KBAICommentInputView alloc] init];
_inputView.placeholder = @"说点什么..."; _inputView.placeholder = @"Send A Message";
_inputView.layer.cornerRadius = 26;
_inputView.clipsToBounds = true;
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self;
_inputView.onSend = ^(NSString *text) { _inputView.onSend = ^(NSString *text) {
[weakSelf sendCommentWithText:text]; [weakSelf sendCommentWithText:text];

View File

@@ -213,7 +213,7 @@
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom]; _replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
_replyButton.titleLabel.font = [UIFont systemFontOfSize:11]; _replyButton.titleLabel.font = [UIFont systemFontOfSize:11];
[_replyButton setTitle:@"回复" forState:UIControlStateNormal]; [_replyButton setTitle:@"回复" forState:UIControlStateNormal];
[_replyButton setTitleColor:[UIColor secondaryLabelColor] forState:UIControlStateNormal]; [_replyButton setTitleColor:[UIColor colorWithHex:0x9F9F9F] forState:UIControlStateNormal];
[_replyButton addTarget:self [_replyButton addTarget:self
action:@selector(replyButtonTapped) action:@selector(replyButtonTapped)
forControlEvents:UIControlEventTouchUpInside]; forControlEvents:UIControlEventTouchUpInside];

View File

@@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 波形条间距 /// 波形条间距
@property(nonatomic, assign) CGFloat barSpacing; @property(nonatomic, assign) CGFloat barSpacing;
/// 自定义柱状高度基准0~1长度需 >= barCount未设置则使用默认算法
@property(nonatomic, strong, nullable) NSArray<NSNumber *> *barHeightPattern;
/// 更新音量值 /// 更新音量值
/// @param rms 音量 RMS 值 (0.0 - 1.0) /// @param rms 音量 RMS 值 (0.0 - 1.0)
- (void)updateWithRMS:(float)rms; - (void)updateWithRMS:(float)rms;

View File

@@ -14,6 +14,8 @@
@property(nonatomic, assign) float currentRMS; @property(nonatomic, assign) float currentRMS;
@property(nonatomic, assign) float targetRMS; @property(nonatomic, assign) float targetRMS;
@property(nonatomic, assign) BOOL isAnimating; @property(nonatomic, assign) BOOL isAnimating;
@property(nonatomic, assign) NSInteger debugFrameCount;
@property(nonatomic, assign) CGSize lastLayoutSize;
@end @end
@implementation KBAiWaveformView @implementation KBAiWaveformView
@@ -49,6 +51,11 @@
- (void)layoutSubviews { - (void)layoutSubviews {
[super layoutSubviews]; [super layoutSubviews];
if (CGSizeEqualToSize(self.lastLayoutSize, self.bounds.size) &&
self.barLayers.count == self.barCount) {
return;
}
self.lastLayoutSize = self.bounds.size;
[self setupBars]; [self setupBars];
} }
@@ -73,10 +80,19 @@
barLayer.cornerRadius = self.barWidth / 2; barLayer.cornerRadius = self.barWidth / 2;
CGFloat x = startX + i * (self.barWidth + self.barSpacing); CGFloat x = startX + i * (self.barWidth + self.barSpacing);
CGFloat height = minHeight; CGFloat height = maxHeight; // transform.scale.y
if (self.barHeightPattern.count > i) {
CGFloat base = [self.barHeightPattern[i] floatValue];
base = MIN(MAX(base, 0.15), 0.9);
height = MAX(minHeight, maxHeight * base);
}
CGFloat y = (maxHeight - height) / 2; CGFloat y = (maxHeight - height) / 2;
barLayer.frame = CGRectMake(x, y, self.barWidth, height); barLayer.frame = CGRectMake(x, 0, self.barWidth, maxHeight);
barLayer.anchorPoint = CGPointMake(0.5, 0.5);
barLayer.position = CGPointMake(x + self.barWidth / 2, maxHeight / 2);
CGFloat scale = height / maxHeight;
barLayer.transform = CATransform3DMakeScale(1, scale, 1);
barLayer.backgroundColor = self.waveColor.CGColor; barLayer.backgroundColor = self.waveColor.CGColor;
[self.layer addSublayer:barLayer]; [self.layer addSublayer:barLayer];
@@ -89,24 +105,42 @@
- (void)updateWithRMS:(float)rms { - (void)updateWithRMS:(float)rms {
self.targetRMS = MIN(MAX(rms, 0), 1); self.targetRMS = MIN(MAX(rms, 0), 1);
NSLog(@"[KBAiWaveformView] updateWithRMS: %.3f, targetRMS=%.3f, barCount=%ld, size=%@",
rms, self.targetRMS, (long)self.barLayers.count, NSStringFromCGRect(self.bounds));
if (!self.displayLink) {
self.currentRMS = self.targetRMS;
[self updateBarsWithRMS:self.currentRMS];
}
} }
- (void)startIdleAnimation { - (void)startIdleAnimation {
if (self.isAnimating) NSLog(@"[KBAiWaveformView] startIdleAnimation (animating=%d, bars=%ld, size=%@)",
self.isAnimating, (long)self.barLayers.count, NSStringFromCGRect(self.bounds));
if (self.isAnimating && self.displayLink) {
return; return;
}
self.isAnimating = YES; self.isAnimating = YES;
self.displayLink = self.debugFrameCount = 0;
[CADisplayLink displayLinkWithTarget:self [self.displayLink invalidate];
selector:@selector(updateAnimation)]; self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(updateAnimation)];
if (@available(iOS 10.0, *)) {
self.displayLink.preferredFramesPerSecond = 60;
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes]; forMode:NSRunLoopCommonModes];
NSLog(@"[KBAiWaveformView] displayLink started");
} }
- (void)stopAnimation { - (void)stopAnimation {
NSLog(@"[KBAiWaveformView] stopAnimation (bars=%ld)", (long)self.barLayers.count);
self.isAnimating = NO; self.isAnimating = NO;
[self.displayLink invalidate]; [self.displayLink invalidate];
self.displayLink = nil; self.displayLink = nil;
for (CAShapeLayer *layer in self.barLayers) {
[layer removeAnimationForKey:@"kb_idle_scale"];
}
} }
- (void)reset { - (void)reset {
@@ -118,8 +152,16 @@
#pragma mark - Animation #pragma mark - Animation
- (void)updateAnimation { - (void)updateAnimation {
if (!self.isAnimating) {
return;
}
self.debugFrameCount += 1;
if (self.debugFrameCount % 30 == 0) {
NSLog(@"[KBAiWaveformView] tick (target=%.3f, current=%.3f, bars=%ld)",
self.targetRMS, self.currentRMS, (long)self.barLayers.count);
}
// RMS // RMS
CGFloat smoothing = 0.3; CGFloat smoothing = 0.65;
self.currentRMS = self.currentRMS =
self.currentRMS + (self.targetRMS - self.currentRMS) * smoothing; self.currentRMS + (self.targetRMS - self.currentRMS) * smoothing;
@@ -139,19 +181,26 @@
// //
CGFloat phase = (CGFloat)i / self.barLayers.count * M_PI * 2; CGFloat phase = (CGFloat)i / self.barLayers.count * M_PI * 2;
CGFloat wave = sin(time * 3 + phase) * 0.3 + 0.7; // 0.4 - 1.0 CGFloat wave = sin(time * 8 + phase) * 0.3 + 0.7; // 0.4 - 1.0
// CGFloat baseFactor = 0.2;
CGFloat heightFactor = rms * wave; if (self.barHeightPattern.count > i) {
CGFloat height = minHeight + range * heightFactor; baseFactor = [self.barHeightPattern[i] floatValue];
baseFactor = MIN(MAX(baseFactor, 0.15), 0.9);
}
//
CGFloat idleWave = sin(time * 10 + phase) * 0.25 + 0.85; // 0.6 - 1.1
CGFloat baseWave = MIN(MAX(baseFactor * idleWave, 0.15), 0.95);
// + RMS
CGFloat dynamicFactor = rms * (0.45 + 0.20 * wave); // 0.45~0.65
CGFloat heightFactor = MIN(1.0, baseWave + dynamicFactor * (1.0 - baseWave));
CGFloat height = maxHeight * heightFactor;
height = MAX(minHeight, MIN(maxHeight, height)); height = MAX(minHeight, MIN(maxHeight, height));
CGFloat scale = height / maxHeight;
//
CGFloat y = (maxHeight - height) / 2;
[CATransaction begin]; [CATransaction begin];
[CATransaction setDisableActions:YES]; [CATransaction setDisableActions:YES];
layer.frame = CGRectMake(layer.frame.origin.x, y, self.barWidth, height); layer.transform = CATransform3DMakeScale(1, scale, 1);
[CATransaction commit]; [CATransaction commit];
} }
} }

View File

@@ -7,6 +7,7 @@
#import "KBVoiceInputBar.h" #import "KBVoiceInputBar.h"
#import "KBAiRecordButton.h" #import "KBAiRecordButton.h"
#import "KBAiWaveformView.h"
#import <Masonry/Masonry.h> #import <Masonry/Masonry.h>
@interface KBVoiceInputBar () <KBAiRecordButtonDelegate> @interface KBVoiceInputBar () <KBAiRecordButtonDelegate>
@@ -34,6 +35,8 @@
/// ///
@property (nonatomic, strong) UIView *recordingView; @property (nonatomic, strong) UIView *recordingView;
@property (nonatomic, strong) UIImageView *recordingCenterIconView; @property (nonatomic, strong) UIImageView *recordingCenterIconView;
@property (nonatomic, strong) KBAiWaveformView *leftWaveformView;
@property (nonatomic, strong) KBAiWaveformView *rightWaveformView;
/// ///
@property (nonatomic, strong) UIView *cancelView; @property (nonatomic, strong) UIView *cancelView;
@@ -146,6 +149,21 @@
make.width.height.mas_equalTo(36); make.width.height.mas_equalTo(36);
}]; }];
[self.recordingView addSubview:self.leftWaveformView];
[self.recordingView addSubview:self.rightWaveformView];
[self.leftWaveformView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.recordingCenterIconView);
make.right.equalTo(self.recordingCenterIconView.mas_left).offset(-16);
make.width.mas_equalTo(84);
make.height.mas_equalTo(34);
}];
[self.rightWaveformView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.recordingCenterIconView);
make.left.equalTo(self.recordingCenterIconView.mas_right).offset(16);
make.width.mas_equalTo(84);
make.height.mas_equalTo(34);
}];
// //
[self.inputContainer addSubview:self.cancelView]; [self.inputContainer addSubview:self.cancelView];
[self.cancelView mas_makeConstraints:^(MASConstraintMaker *make) { [self.cancelView mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -210,6 +228,7 @@
- (void)setInputState:(KBVoiceInputBarState)inputState { - (void)setInputState:(KBVoiceInputBarState)inputState {
_inputState = inputState; _inputState = inputState;
NSLog(@"[KBVoiceInputBar] setInputState: %ld", (long)inputState);
self.textInputView.hidden = (inputState != KBVoiceInputBarStateText); self.textInputView.hidden = (inputState != KBVoiceInputBarStateText);
self.voiceInputView.hidden = (inputState != KBVoiceInputBarStateVoice); self.voiceInputView.hidden = (inputState != KBVoiceInputBarStateVoice);
self.recordingView.hidden = (inputState != KBVoiceInputBarStateRecording); self.recordingView.hidden = (inputState != KBVoiceInputBarStateRecording);
@@ -224,6 +243,11 @@
if (!self.toggleIconButton.hidden) { if (!self.toggleIconButton.hidden) {
[self.inputContainer bringSubviewToFront:self.toggleIconButton]; [self.inputContainer bringSubviewToFront:self.toggleIconButton];
} }
if (inputState == KBVoiceInputBarStateRecording) {
[self startRecordingWaveAnimationIfNeeded];
} else {
[self stopRecordingWaveAnimation];
}
[self updateCenterTextIfNeeded]; [self updateCenterTextIfNeeded];
} }
@@ -231,6 +255,12 @@
- (void)updateVolumeRMS:(float)rms { - (void)updateVolumeRMS:(float)rms {
[self.recordButton updateVolumeRMS:rms]; [self.recordButton updateVolumeRMS:rms];
if (self.inputState == KBVoiceInputBarStateRecording) {
CGFloat safeRMS = MAX(rms, 0.6f);
NSLog(@"[KBVoiceInputBar] updateVolumeRMS: %.3f (safe=%.3f)", rms, safeRMS);
[self.leftWaveformView updateWithRMS:safeRMS];
[self.rightWaveformView updateWithRMS:safeRMS];
}
} }
#pragma mark - KBAiRecordButtonDelegate #pragma mark - KBAiRecordButtonDelegate
@@ -370,6 +400,30 @@
return _recordingCenterIconView; return _recordingCenterIconView;
} }
- (KBAiWaveformView *)leftWaveformView {
if (!_leftWaveformView) {
_leftWaveformView = [[KBAiWaveformView alloc] init];
_leftWaveformView.waveColor = [UIColor whiteColor];
_leftWaveformView.barCount = 7;
_leftWaveformView.barWidth = 3;
_leftWaveformView.barSpacing = 6;
_leftWaveformView.barHeightPattern = @[@0.35, @0.85, @0.5, @0.35, @0.9, @0.55, @0.35];
}
return _leftWaveformView;
}
- (KBAiWaveformView *)rightWaveformView {
if (!_rightWaveformView) {
_rightWaveformView = [[KBAiWaveformView alloc] init];
_rightWaveformView.waveColor = [UIColor whiteColor];
_rightWaveformView.barCount = 7;
_rightWaveformView.barWidth = 3;
_rightWaveformView.barSpacing = 6;
_rightWaveformView.barHeightPattern = @[@0.35, @0.85, @0.5, @0.35, @0.9, @0.55, @0.35];
}
return _rightWaveformView;
}
- (UIView *)cancelView { - (UIView *)cancelView {
if (!_cancelView) { if (!_cancelView) {
_cancelView = [[UIView alloc] init]; _cancelView = [[UIView alloc] init];
@@ -478,4 +532,37 @@
} }
} }
#pragma mark - Recording Wave
- (void)startRecordingWaveAnimationIfNeeded {
NSLog(@"[KBVoiceInputBar] startRecordingWaveAnimationIfNeeded");
self.leftWaveformView.hidden = NO;
self.rightWaveformView.hidden = NO;
[self.inputContainer setNeedsLayout];
[self.recordingView setNeedsLayout];
[self.inputContainer layoutIfNeeded];
[self.recordingView layoutIfNeeded];
[self.leftWaveformView setNeedsLayout];
[self.rightWaveformView setNeedsLayout];
[self.leftWaveformView layoutIfNeeded];
[self.rightWaveformView layoutIfNeeded];
NSLog(@"[KBVoiceInputBar] waveform frames L=%@ R=%@",
NSStringFromCGRect(self.leftWaveformView.frame),
NSStringFromCGRect(self.rightWaveformView.frame));
[self.leftWaveformView startIdleAnimation];
[self.rightWaveformView startIdleAnimation];
[self.leftWaveformView updateWithRMS:0.7f];
[self.rightWaveformView updateWithRMS:0.7f];
}
- (void)stopRecordingWaveAnimation {
NSLog(@"[KBVoiceInputBar] stopRecordingWaveAnimation");
[self.leftWaveformView stopAnimation];
[self.rightWaveformView stopAnimation];
[self.leftWaveformView reset];
[self.rightWaveformView reset];
self.leftWaveformView.hidden = YES;
self.rightWaveformView.hidden = YES;
}
@end @end

View File

@@ -46,7 +46,7 @@
[super viewDidLoad]; [super viewDidLoad];
self.kb_navView.hidden = YES; self.kb_navView.hidden = YES;
self.view.backgroundColor = [UIColor blackColor]; // self.view.backgroundColor = [UIColor blackColor];
self.aiVM = [[AiVM alloc] init]; self.aiVM = [[AiVM alloc] init];
/// 1 /// 1
@@ -261,7 +261,7 @@
- (UIButton *)closeButton { - (UIButton *)closeButton {
if (!_closeButton) { if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; _closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setImage:[UIImage imageNamed:@"comment_close_icon"] forState:UIControlStateNormal]; [_closeButton setImage:[UIImage imageNamed:@"ai_close_icon"] forState:UIControlStateNormal];
[_closeButton addTarget:self action:@selector(closeButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [_closeButton addTarget:self action:@selector(closeButtonTapped) forControlEvents:UIControlEventTouchUpInside];
} }
return _closeButton; return _closeButton;

View File

@@ -6,6 +6,7 @@
// //
#import "AIReportVC.h" #import "AIReportVC.h"
#import "AiVM.h"
#pragma mark - AIReportOptionCell #pragma mark - AIReportOptionCell
@@ -116,6 +117,8 @@
/// ///
@property (nonatomic, strong) UIButton *submitButton; @property (nonatomic, strong) UIButton *submitButton;
@property (nonatomic, strong) AiVM *viewModel;
@end @end
@implementation AIReportVC @implementation AIReportVC
@@ -134,6 +137,8 @@
[self setupUI]; [self setupUI];
/// 3 /// 3
[self bindActions]; [self bindActions];
/// 4/
[self bindKeyboardNotifications];
} }
#pragma mark - 1 #pragma mark - 1
@@ -286,6 +291,59 @@
[self.view endEditing:YES]; [self.view endEditing:YES];
} }
#pragma mark - 4
- (void)bindKeyboardNotifications {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleKeyboardWillChange:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleKeyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)handleKeyboardWillChange:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo ?: @{};
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
NSInteger curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
CGRect endFrameInView = [self.view convertRect:endFrame fromView:nil];
CGFloat keyboardHeight = MAX(0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(endFrameInView));
UIEdgeInsets inset = self.scrollView.contentInset;
inset.bottom = keyboardHeight;
UIViewAnimationOptions options = (UIViewAnimationOptions)(curve << 16);
[UIView animateWithDuration:duration delay:0 options:options animations:^{
self.scrollView.contentInset = inset;
self.scrollView.scrollIndicatorInsets = inset;
if ([self.descriptionTextView isFirstResponder]) {
CGRect rect = [self.descriptionTextView convertRect:self.descriptionTextView.bounds toView:self.scrollView];
rect = CGRectInset(rect, 0, -12);
[self.scrollView scrollRectToVisible:rect animated:NO];
}
} completion:nil];
}
- (void)handleKeyboardWillHide:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo ?: @{};
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
NSInteger curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
UIViewAnimationOptions options = (UIViewAnimationOptions)(curve << 16);
[UIView animateWithDuration:duration delay:0 options:options animations:^{
self.scrollView.contentInset = UIEdgeInsetsZero;
self.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
} completion:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UITableViewDataSource #pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
@@ -351,6 +409,11 @@
#pragma mark - Actions #pragma mark - Actions
- (void)submitButtonTapped { - (void)submitButtonTapped {
if (self.personaId <= 0) {
[KBHUD showError:KBLocalized(@"Invalid parameter")];
return;
}
if (self.selectedReasonIndexes.count == 0) { if (self.selectedReasonIndexes.count == 0) {
[KBHUD showError:KBLocalized(@"Please select at least one report reason")]; [KBHUD showError:KBLocalized(@"Please select at least one report reason")];
return; return;
@@ -373,19 +436,53 @@
[selectedContents addObject:self.contentOptions[index.integerValue]]; [selectedContents addObject:self.contentOptions[index.integerValue]];
} }
NSString *description = self.descriptionTextView.text ?: @""; NSString *reportDesc = [self.descriptionTextView.text ?: @"" stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSLog(@"[AIReportVC] 举报人设 ID: %ld", (long)self.personaId); NSLog(@"[AIReportVC] 举报人设 ID: %ld", (long)self.personaId);
NSLog(@"[AIReportVC] 举报原因: %@", selectedReasons); NSLog(@"[AIReportVC] 举报原因: %@", selectedReasons);
NSLog(@"[AIReportVC] 内容类型: %@", selectedContents); NSLog(@"[AIReportVC] 内容类型: %@", selectedContents);
NSLog(@"[AIReportVC] 描述: %@", description); NSLog(@"[AIReportVC] 描述: %@", reportDesc);
// TODO: // 1-12 1-9 10-12
[KBHUD showSuccess:KBLocalized(@"Report submitted")]; NSMutableArray<NSNumber *> *reportTypes = [NSMutableArray array];
NSArray<NSNumber *> *sortedReasonIndexes = [[self.selectedReasonIndexes allObjects]
sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES]]];
for (NSNumber *index in sortedReasonIndexes) {
NSInteger type = index.integerValue + 1;
if (type > 0) {
[reportTypes addObject:@(type)];
}
}
NSArray<NSNumber *> *sortedContentIndexes = [[self.selectedContentIndexes allObjects]
sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES]]];
for (NSNumber *index in sortedContentIndexes) {
NSInteger type = self.reportReasons.count + index.integerValue + 1;
if (type > 0) {
[reportTypes addObject:@(type)];
}
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [KBHUD show];
[self.navigationController popViewControllerAnimated:YES]; __weak typeof(self) weakSelf = self;
}); [self.viewModel reportCompanionWithCompanionId:self.personaId
reportTypes:reportTypes
reportDesc:reportDesc
chatContext:nil
evidenceImageUrl:nil
completion:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
if (!success) {
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
[KBHUD showError:msg];
return;
}
[KBHUD showSuccess:KBLocalized(@"Report submitted")];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf.navigationController popViewControllerAnimated:YES];
});
});
}];
} }
#pragma mark - Lazy Load #pragma mark - Lazy Load
@@ -395,6 +492,7 @@
_scrollView = [[UIScrollView alloc] init]; _scrollView = [[UIScrollView alloc] init];
_scrollView.showsVerticalScrollIndicator = NO; _scrollView.showsVerticalScrollIndicator = NO;
_scrollView.alwaysBounceVertical = YES; _scrollView.alwaysBounceVertical = YES;
_scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
} }
return _scrollView; return _scrollView;
} }
@@ -536,4 +634,11 @@
return _submitButton; return _submitButton;
} }
- (AiVM *)viewModel {
if (!_viewModel) {
_viewModel = [[AiVM alloc] init];
}
return _viewModel;
}
@end @end

View File

@@ -18,10 +18,11 @@
#import "KBUserSessionManager.h" #import "KBUserSessionManager.h"
#import "LSTPopView.h" #import "LSTPopView.h"
#import "KBAIMessageVC.h" #import "KBAIMessageVC.h"
#import "KBAICommentInputView.h"
#import <Masonry/Masonry.h> #import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h> #import <SDWebImage/SDWebImage.h>
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate, UITextViewDelegate> @interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate>
/// ///
@property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UICollectionView *collectionView;
@@ -37,16 +38,10 @@
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap; @property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
@property (nonatomic, weak) LSTPopView *chatLimitPopView; @property (nonatomic, weak) LSTPopView *chatLimitPopView;
/// ///
@property (nonatomic, strong) UIView *textInputContainerView; @property (nonatomic, strong) KBAICommentInputView *commentInputView;
/// ///
@property (nonatomic, strong) UITextView *textInputTextView; @property (nonatomic, strong) MASConstraint *commentInputBottomConstraint;
///
@property (nonatomic, strong) UIButton *sendButton;
///
@property (nonatomic, strong) UILabel *placeholderLabel;
///
@property (nonatomic, strong) MASConstraint *textInputContainerBottomConstraint;
/// ///
@property (nonatomic, assign) BOOL isTextInputMode; @property (nonatomic, assign) BOOL isTextInputMode;
@@ -87,6 +82,12 @@
@property (nonatomic, assign) NSInteger pendingAIRequestCount; @property (nonatomic, assign) NSInteger pendingAIRequestCount;
/// /
@property (nonatomic, assign) BOOL isVoiceProcessing;
///
@property (nonatomic, assign) BOOL isVoiceRecording;
/// ///
@property (nonatomic, strong) UIButton *messageButton; @property (nonatomic, strong) UIButton *messageButton;
@@ -116,8 +117,8 @@
if (!firstResponder) { if (!firstResponder) {
return NO; return NO;
} }
// textInputTextView // commentInputView
if (firstResponder == self.textInputTextView) { if ([firstResponder isDescendantOfView:self.commentInputView]) {
return YES; return YES;
} }
return [firstResponder isDescendantOfView:self.voiceInputBar]; return [firstResponder isDescendantOfView:self.voiceInputBar];
@@ -159,6 +160,16 @@
} }
} }
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
if (cell) {
[cell onResignedCurrentPersonaCell];
}
}
}
- (void)viewDidLayoutSubviews { - (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews]; [super viewDidLayoutSubviews];
if (self.bottomMaskLayer) { if (self.bottomMaskLayer) {
@@ -169,7 +180,7 @@
#pragma mark - 1 #pragma mark - 1
- (void)setupUI { - (void)setupUI {
self.voiceInputBarHeight = 80.0; self.voiceInputBarHeight = 52;
self.baseInputBarBottomSpacing = KB_TABBAR_HEIGHT; self.baseInputBarBottomSpacing = KB_TABBAR_HEIGHT;
[self.view addSubview:self.collectionView]; [self.view addSubview:self.collectionView];
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -207,37 +218,13 @@
/// ///
- (void)setupTextInputView { - (void)setupTextInputView {
// //
[self.view addSubview:self.textInputContainerView]; [self.view addSubview:self.commentInputView];
[self.textInputContainerView mas_makeConstraints:^(MASConstraintMaker *make) { [self.commentInputView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view); make.left.equalTo(self.view).offset(12);
self.textInputContainerBottomConstraint = make.bottom.equalTo(self.view).offset(100); // make.right.equalTo(self.view).offset(-12);
make.height.mas_greaterThanOrEqualTo(50); self.commentInputBottomConstraint = make.bottom.equalTo(self.view).offset(100); //
}]; make.height.mas_equalTo(self.voiceInputBarHeight);
[self.textInputContainerView addSubview:self.textInputTextView];
[self.textInputContainerView addSubview:self.sendButton];
[self.textInputTextView addSubview:self.placeholderLabel];
[self.sendButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.textInputContainerView).offset(-16);
make.bottom.equalTo(self.textInputContainerView).offset(-10);
make.width.mas_equalTo(60);
make.height.mas_equalTo(36);
}];
[self.textInputTextView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.textInputContainerView).offset(16);
make.right.equalTo(self.sendButton.mas_left).offset(-10);
make.top.equalTo(self.textInputContainerView).offset(8);
make.bottom.equalTo(self.textInputContainerView).offset(-8);
make.height.mas_greaterThanOrEqualTo(36);
make.height.mas_lessThanOrEqualTo(100);
}];
[self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.textInputTextView).offset(15);
make.top.equalTo(self.textInputTextView).offset(8);
}]; }];
} }
@@ -254,18 +241,17 @@
- (void)showTextInputView { - (void)showTextInputView {
self.isTextInputMode = YES; self.isTextInputMode = YES;
self.voiceInputBar.hidden = YES; self.voiceInputBar.hidden = YES;
self.textInputContainerView.hidden = NO; self.commentInputView.hidden = NO;
[self.textInputTextView becomeFirstResponder]; [self.commentInputView showKeyboard];
} }
/// ///
- (void)hideTextInputView { - (void)hideTextInputView {
self.isTextInputMode = NO; self.isTextInputMode = NO;
[self.textInputTextView resignFirstResponder]; [self.view endEditing:YES];
self.textInputContainerView.hidden = YES; [self.commentInputView clearText];
self.commentInputView.hidden = YES;
self.voiceInputBar.hidden = NO; self.voiceInputBar.hidden = NO;
self.textInputTextView.text = @"";
self.placeholderLabel.hidden = NO;
} }
#pragma mark - 2 #pragma mark - 2
@@ -511,8 +497,7 @@
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if (self.isWaitingForAIResponse) { if (self.isWaitingForAIResponse) {
NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动"); NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动");
scrollView.scrollEnabled = NO; [self updateCollectionViewScrollState];
scrollView.scrollEnabled = YES;
} }
} }
@@ -575,11 +560,11 @@
bottomSpacing = keyboardHeight - 5.0; bottomSpacing = keyboardHeight - 5.0;
// //
if (self.isTextInputMode) { if (self.isTextInputMode) {
[self.textInputContainerBottomConstraint setOffset:-keyboardHeight]; [self.commentInputBottomConstraint setOffset:-keyboardHeight];
} }
} else { } else {
bottomSpacing = self.baseInputBarBottomSpacing; bottomSpacing = self.baseInputBarBottomSpacing;
[self.textInputContainerBottomConstraint setOffset:100]; // [self.commentInputBottomConstraint setOffset:100]; //
} }
[self.voiceInputBarBottomConstraint setOffset:-bottomSpacing]; [self.voiceInputBarBottomConstraint setOffset:-bottomSpacing];
@@ -614,7 +599,7 @@
if ([touch.view isDescendantOfView:self.voiceInputBar]) { if ([touch.view isDescendantOfView:self.voiceInputBar]) {
return NO; return NO;
} }
if ([touch.view isDescendantOfView:self.textInputContainerView]) { if ([touch.view isDescendantOfView:self.commentInputView]) {
return NO; return NO;
} }
return YES; return YES;
@@ -661,6 +646,14 @@
#pragma mark - Private #pragma mark - Private
- (void)updateCollectionViewScrollState {
BOOL shouldEnable = !self.isWaitingForAIResponse
&& !self.isVoiceRecording
&& !self.isVoiceProcessing;
self.collectionView.scrollEnabled = shouldEnable;
self.collectionView.panGestureRecognizer.enabled = shouldEnable;
}
- (void)updateChatViewBottomInset { - (void)updateChatViewBottomInset {
CGFloat bottomInset; CGFloat bottomInset;
@@ -708,18 +701,6 @@
[pop pop]; [pop pop];
} }
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
self.placeholderLabel.hidden = textView.text.length > 0;
//
CGSize size = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
CGFloat newHeight = MIN(MAX(size.height, 36), 100);
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_greaterThanOrEqualTo(newHeight);
}];
}
#pragma mark - Lazy Load #pragma mark - Lazy Load
- (UICollectionView *)collectionView { - (UICollectionView *)collectionView {
@@ -753,53 +734,19 @@
return _voiceInputBar; return _voiceInputBar;
} }
- (UIView *)textInputContainerView { - (KBAICommentInputView *)commentInputView {
if (!_textInputContainerView) { if (!_commentInputView) {
_textInputContainerView = [[UIView alloc] init]; _commentInputView = [[KBAICommentInputView alloc] init];
_textInputContainerView.backgroundColor = [UIColor whiteColor]; _commentInputView.layer.cornerRadius = 26;
_textInputContainerView.hidden = YES; _commentInputView.layer.masksToBounds = true;
_commentInputView.hidden = YES;
_commentInputView.placeholder = KBLocalized(@"send a message");
__weak typeof(self) weakSelf = self;
_commentInputView.onSend = ^(NSString *text) {
[weakSelf handleCommentInputSend:text];
};
} }
return _textInputContainerView; return _commentInputView;
}
- (UITextView *)textInputTextView {
if (!_textInputTextView) {
_textInputTextView = [[UITextView alloc] init];
_textInputTextView.font = [UIFont systemFontOfSize:16];
_textInputTextView.textColor = [UIColor blackColor];
_textInputTextView.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0];
_textInputTextView.layer.cornerRadius = 18;
_textInputTextView.layer.masksToBounds = YES;
_textInputTextView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
_textInputTextView.delegate = self;
_textInputTextView.returnKeyType = UIReturnKeySend;
_textInputTextView.enablesReturnKeyAutomatically = YES;
}
return _textInputTextView;
}
- (UIButton *)sendButton {
if (!_sendButton) {
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_sendButton setTitle:KBLocalized(@"发送") forState:UIControlStateNormal];
[_sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_sendButton.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
_sendButton.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:1.0];
_sendButton.layer.cornerRadius = 18;
_sendButton.layer.masksToBounds = YES;
[_sendButton addTarget:self action:@selector(sendButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _sendButton;
}
- (UILabel *)placeholderLabel {
if (!_placeholderLabel) {
_placeholderLabel = [[UILabel alloc] init];
_placeholderLabel.text = KBLocalized(@"输入消息...");
_placeholderLabel.font = [UIFont systemFontOfSize:16];
_placeholderLabel.textColor = [UIColor lightGrayColor];
}
return _placeholderLabel;
} }
#pragma mark - KBChatLimitPopViewDelegate #pragma mark - KBChatLimitPopViewDelegate
@@ -866,22 +813,18 @@
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
} }
/// - handleTranscribedText /// - handleTranscribedText
- (void)sendButtonTapped { - (void)handleCommentInputSend:(NSString *)text {
NSString *text = [self.textInputTextView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *trimmedText = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (text.length == 0) { if (trimmedText.length == 0) {
return; return;
} }
//
self.textInputTextView.text = @"";
self.placeholderLabel.hidden = NO;
// //
[self hideTextInputView]; [self hideTextInputView];
// handleTranscribedText // handleTranscribedText
[self handleTranscribedText:text]; [self handleTranscribedText:trimmedText];
} }
#pragma mark - KBVoiceToTextManagerDelegate #pragma mark - KBVoiceToTextManagerDelegate
@@ -897,14 +840,23 @@
} }
- (void)voiceToTextManagerDidBeginRecording:(KBVoiceToTextManager *)manager { - (void)voiceToTextManagerDidBeginRecording:(KBVoiceToTextManager *)manager {
self.isVoiceRecording = YES;
self.isVoiceProcessing = YES;
[self updateCollectionViewScrollState];
[self.voiceRecordManager startRecording]; [self.voiceRecordManager startRecording];
} }
- (void)voiceToTextManagerDidEndRecording:(KBVoiceToTextManager *)manager { - (void)voiceToTextManagerDidEndRecording:(KBVoiceToTextManager *)manager {
self.isVoiceRecording = NO;
self.isVoiceProcessing = YES;
[self updateCollectionViewScrollState];
[self.voiceRecordManager stopRecording]; [self.voiceRecordManager stopRecording];
} }
- (void)voiceToTextManagerDidCancelRecording:(KBVoiceToTextManager *)manager { - (void)voiceToTextManagerDidCancelRecording:(KBVoiceToTextManager *)manager {
self.isVoiceRecording = NO;
self.isVoiceProcessing = NO;
[self updateCollectionViewScrollState];
[self.voiceRecordManager cancelRecording]; [self.voiceRecordManager cancelRecording];
} }
@@ -941,6 +893,8 @@
if (cell) { if (cell) {
[cell updateLastUserMessage:KBLocalized(@"语音识别失败")]; [cell updateLastUserMessage:KBLocalized(@"语音识别失败")];
} }
strongSelf.isVoiceProcessing = NO;
[strongSelf updateCollectionViewScrollState];
return; return;
} }
@@ -949,8 +903,10 @@
NSLog(@"[KBAIHomeVC] 语音转文字结果为空"); NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
[KBHUD showError:KBLocalized(@"未识别到语音内容")]; [KBHUD showError:KBLocalized(@"未识别到语音内容")];
if (cell) { if (cell) {
[cell updateLastUserMessage:KBLocalized(@"未识别到语音")]; [cell removeLoadingUserMessage];
} }
strongSelf.isVoiceProcessing = NO;
[strongSelf updateCollectionViewScrollState];
return; return;
} }
@@ -958,6 +914,7 @@
[cell updateLastUserMessage:transcript]; [cell updateLastUserMessage:transcript];
} }
strongSelf.isVoiceProcessing = NO;
[strongSelf handleTranscribedText:transcript appendToUI:NO]; [strongSelf handleTranscribedText:transcript appendToUI:NO];
}); });
}]; }];
@@ -971,6 +928,9 @@
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager - (void)voiceRecordManager:(KBVoiceRecordManager *)manager
didFailWithError:(NSError *)error { didFailWithError:(NSError *)error {
NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription); NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription);
self.isVoiceRecording = NO;
self.isVoiceProcessing = NO;
[self updateCollectionViewScrollState];
} }
#pragma mark - Private #pragma mark - Private
@@ -1001,7 +961,7 @@
self.pendingAIRequestCount += 1; self.pendingAIRequestCount += 1;
self.isWaitingForAIResponse = (self.pendingAIRequestCount > 0); self.isWaitingForAIResponse = (self.pendingAIRequestCount > 0);
if (self.pendingAIRequestCount == 1) { if (self.pendingAIRequestCount == 1) {
self.collectionView.scrollEnabled = NO; [self updateCollectionViewScrollState];
NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动"); NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动");
} }
@@ -1020,7 +980,7 @@
} }
strongSelf.isWaitingForAIResponse = (strongSelf.pendingAIRequestCount > 0); strongSelf.isWaitingForAIResponse = (strongSelf.pendingAIRequestCount > 0);
if (strongSelf.pendingAIRequestCount == 0) { if (strongSelf.pendingAIRequestCount == 0) {
strongSelf.collectionView.scrollEnabled = YES; [strongSelf updateCollectionViewScrollState];
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
} }

View File

@@ -23,7 +23,7 @@
@property (nonatomic, strong) JXCategoryListContainerView *listContainerView; @property (nonatomic, strong) JXCategoryListContainerView *listContainerView;
/// ///
@property (nonatomic, strong) UIButton *searchButton; //@property (nonatomic, strong) UIButton *searchButton;
/// ///
@property (nonatomic, strong) NSArray<NSString *> *titles; @property (nonatomic, strong) NSArray<NSString *> *titles;
@@ -42,7 +42,7 @@
[self setupUI]; [self setupUI];
/// 2 /// 2
[self bindActions]; // [self bindActions];
} }
#pragma mark - 1 #pragma mark - 1
@@ -69,12 +69,12 @@
}]; }];
// //
[self.kb_navView addSubview:self.searchButton]; // [self.kb_navView addSubview:self.searchButton];
[self.searchButton mas_makeConstraints:^(MASConstraintMaker *make) { // [self.searchButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.kb_navView).offset(-16); // make.right.equalTo(self.kb_navView).offset(-16);
make.centerY.equalTo(self.kb_backButton); // make.centerY.equalTo(self.kb_backButton);
make.width.height.mas_equalTo(24); // make.width.height.mas_equalTo(24);
}]; // }];
// //
[self.view addSubview:self.listContainerView]; [self.view addSubview:self.listContainerView];
@@ -94,9 +94,9 @@
#pragma mark - 2 #pragma mark - 2
- (void)bindActions { //- (void)bindActions {
[self.searchButton addTarget:self action:@selector(searchButtonTapped) forControlEvents:UIControlEventTouchUpInside]; // [self.searchButton addTarget:self action:@selector(searchButtonTapped) forControlEvents:UIControlEventTouchUpInside];
} //}
#pragma mark - Actions #pragma mark - Actions
@@ -189,16 +189,16 @@
return _listContainerView; return _listContainerView;
} }
- (UIButton *)searchButton { //- (UIButton *)searchButton {
if (!_searchButton) { // if (!_searchButton) {
_searchButton = [UIButton buttonWithType:UIButtonTypeCustom]; // _searchButton = [UIButton buttonWithType:UIButtonTypeCustom];
if (@available(iOS 13.0, *)) { // if (@available(iOS 13.0, *)) {
UIImage *searchImage = [UIImage systemImageNamed:@"magnifyingglass"]; // UIImage *searchImage = [UIImage systemImageNamed:@"magnifyingglass"];
[_searchButton setImage:searchImage forState:UIControlStateNormal]; // [_searchButton setImage:searchImage forState:UIControlStateNormal];
_searchButton.tintColor = [UIColor colorWithHex:0x1B1F1A]; // _searchButton.tintColor = [UIColor colorWithHex:0x1B1F1A];
} // }
} // }
return _searchButton; // return _searchButton;
} //}
@end @end

View File

@@ -173,6 +173,22 @@ typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nu
- (void)fetchCompanionDetailWithCompanionId:(NSInteger)companionId - (void)fetchCompanionDetailWithCompanionId:(NSInteger)companionId
completion:(void(^)(KBAICompanionDetailModel * _Nullable detail, NSError * _Nullable error))completion; completion:(void(^)(KBAICompanionDetailModel * _Nullable detail, NSError * _Nullable error))completion;
#pragma mark - 举报接口
/// 举报 AI 角色
/// @param companionId AI 角色 ID
/// @param reportTypes 举报类型数组(按界面从上到下 1-12
/// @param reportDesc 详细描述
/// @param chatContext 聊天上下文快照 JSON 字符串
/// @param evidenceImageUrl 图片证据 URL
/// @param completion 完成回调
- (void)reportCompanionWithCompanionId:(NSInteger)companionId
reportTypes:(NSArray<NSNumber *> *)reportTypes
reportDesc:(nullable NSString *)reportDesc
chatContext:(nullable NSString *)chatContext
evidenceImageUrl:(nullable NSString *)evidenceImageUrl
completion:(void(^)(BOOL success, NSError * _Nullable error))completion;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -808,4 +808,100 @@ autoShowBusinessError:NO
}]; }];
} }
#pragma mark -
- (void)reportCompanionWithCompanionId:(NSInteger)companionId
reportTypes:(NSArray<NSNumber *> *)reportTypes
reportDesc:(nullable NSString *)reportDesc
chatContext:(nullable NSString *)chatContext
evidenceImageUrl:(nullable NSString *)evidenceImageUrl
completion:(void (^)(BOOL, NSError * _Nullable))completion {
if (companionId <= 0) {
NSError *error = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"invalid companionId"}];
if (completion) {
completion(NO, error);
}
return;
}
NSMutableArray<NSNumber *> *typeList = [NSMutableArray array];
for (id item in reportTypes) {
if ([item isKindOfClass:[NSNumber class]]) {
[typeList addObject:(NSNumber *)item];
} else if ([item isKindOfClass:[NSString class]]) {
NSInteger value = [(NSString *)item integerValue];
[typeList addObject:@(value)];
}
}
if (typeList.count == 0) {
NSError *error = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"reportTypes is empty"}];
if (completion) {
completion(NO, error);
}
return;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"companionId"] = @(companionId);
params[@"reportTypes"] = [typeList copy];
if (reportDesc.length > 0) {
params[@"reportDesc"] = reportDesc;
}
if (chatContext.length > 0) {
params[@"chatContext"] = chatContext;
}
if (evidenceImageUrl.length > 0) {
params[@"evidenceImageUrl"] = evidenceImageUrl;
}
NSLog(@"[AiVM] /ai-companion/report request: %@", params);
[[KBNetworkManager shared]
POST:@"/ai-companion/report"
jsonBody:[params copy]
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary *_Nullable json,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error) {
NSLog(@"[AiVM] /ai-companion/report failed: %@", error.localizedDescription ?: @"");
if (completion) {
completion(NO, error);
}
return;
}
NSLog(@"[AiVM] /ai-companion/report response: %@", json);
if (![json isKindOfClass:[NSDictionary class]]) {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"数据格式错误"}];
if (completion) {
completion(NO, parseError);
}
return;
}
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey : message}];
if (completion) {
completion(NO, bizError);
}
return;
}
if (completion) {
completion(YES, nil);
}
}];
}
@end @end

View File

@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface UIColor (Extension) @interface UIColor (Extension)
+ (UIColor *)colorWithHex:(int)hexValue; + (UIColor *)colorWithHex:(int)hexValue;
+ (UIColor *)colorWithHex:(int)hexValue alpha:(CGFloat)alpha;
+ (nullable UIColor *)colorWithHexString:(NSString *)hexString; + (nullable UIColor *)colorWithHexString:(NSString *)hexString;
+ (UIColor *)kb_dynamicColorWithLightColor:(UIColor *)lightColor + (UIColor *)kb_dynamicColorWithLightColor:(UIColor *)lightColor
darkColor:(UIColor *)darkColor; darkColor:(UIColor *)darkColor;