1
This commit is contained in:
6
keyBoard/Assets.xcassets/AI/Contents.json
Normal file
6
keyBoard/Assets.xcassets/AI/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
22
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json
vendored
Normal file
22
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_jianpan_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_jianpan_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@2x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@3x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
22
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json
vendored
Normal file
22
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_luyining_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_luyining_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@2x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@3x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
22
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json
vendored
Normal file
22
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_maikefeng_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ai_maikefeng_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@2x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@3x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -47,6 +47,12 @@ typedef NS_ENUM(NSInteger, KBAiRecordButtonState) {
|
||||
/// 主色调
|
||||
@property(nonatomic, strong) UIColor *tintColor;
|
||||
|
||||
/// 正常状态左侧图标
|
||||
@property(nonatomic, strong, nullable) UIImage *normalIconImage;
|
||||
|
||||
/// 录音中状态图标(居中显示)
|
||||
@property(nonatomic, strong, nullable) UIImage *recordingIconImage;
|
||||
|
||||
/// 更新音量(用于波形动画)
|
||||
/// @param rms 音量 RMS 值 (0.0 - 1.0)
|
||||
- (void)updateVolumeRMS:(float)rms;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@property(nonatomic, strong) UILabel *titleLabel;
|
||||
@property(nonatomic, strong) KBAiWaveformView *waveformView;
|
||||
@property(nonatomic, strong) UIImageView *micIconView;
|
||||
@property(nonatomic, strong) UIImageView *recordingIconView;
|
||||
@property(nonatomic, assign) BOOL isPressing;
|
||||
|
||||
@end
|
||||
@@ -50,7 +51,7 @@
|
||||
self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:self.backgroundView];
|
||||
|
||||
// 麦克风图标
|
||||
// 麦克风图标(正常态左侧)
|
||||
self.micIconView = [[UIImageView alloc] init];
|
||||
self.micIconView.image = [UIImage systemImageNamed:@"mic.fill"];
|
||||
self.micIconView.tintColor = self.tintColor;
|
||||
@@ -58,6 +59,13 @@
|
||||
self.micIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.backgroundView addSubview:self.micIconView];
|
||||
|
||||
// 录音中图标(居中)
|
||||
self.recordingIconView = [[UIImageView alloc] init];
|
||||
self.recordingIconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
self.recordingIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.recordingIconView.hidden = YES;
|
||||
[self.backgroundView addSubview:self.recordingIconView];
|
||||
|
||||
// 标题标签
|
||||
self.titleLabel = [[UILabel alloc] init];
|
||||
self.titleLabel.text = self.normalTitle;
|
||||
@@ -104,6 +112,13 @@
|
||||
constraintEqualToAnchor:self.backgroundView.centerYAnchor],
|
||||
[self.waveformView.widthAnchor constraintEqualToConstant:60],
|
||||
[self.waveformView.heightAnchor constraintEqualToConstant:30],
|
||||
|
||||
[self.recordingIconView.centerXAnchor
|
||||
constraintEqualToAnchor:self.backgroundView.centerXAnchor],
|
||||
[self.recordingIconView.centerYAnchor
|
||||
constraintEqualToAnchor:self.backgroundView.centerYAnchor],
|
||||
[self.recordingIconView.widthAnchor constraintEqualToConstant:36],
|
||||
[self.recordingIconView.heightAnchor constraintEqualToConstant:36],
|
||||
]];
|
||||
|
||||
// 添加手势
|
||||
@@ -131,6 +146,18 @@
|
||||
self.waveformView.waveColor = tintColor;
|
||||
}
|
||||
|
||||
- (void)setNormalIconImage:(UIImage *)normalIconImage {
|
||||
_normalIconImage = normalIconImage;
|
||||
if (normalIconImage) {
|
||||
self.micIconView.image = normalIconImage;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRecordingIconImage:(UIImage *)recordingIconImage {
|
||||
_recordingIconImage = recordingIconImage;
|
||||
self.recordingIconView.image = recordingIconImage;
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)updateVolumeRMS:(float)rms {
|
||||
@@ -144,18 +171,21 @@
|
||||
case KBAiRecordButtonStateNormal:
|
||||
self.titleLabel.text = self.normalTitle;
|
||||
self.backgroundView.backgroundColor = [UIColor systemGray6Color];
|
||||
self.micIconView.alpha = 1;
|
||||
self.micIconView.hidden = NO;
|
||||
self.titleLabel.hidden = NO;
|
||||
self.recordingIconView.hidden = YES;
|
||||
self.waveformView.alpha = 0;
|
||||
[self.waveformView stopAnimation];
|
||||
break;
|
||||
|
||||
case KBAiRecordButtonStateRecording:
|
||||
self.titleLabel.text = self.recordingTitle;
|
||||
self.backgroundView.backgroundColor =
|
||||
[self.tintColor colorWithAlphaComponent:0.15];
|
||||
self.micIconView.alpha = 1;
|
||||
self.waveformView.alpha = 1;
|
||||
[self.waveformView startIdleAnimation];
|
||||
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6];
|
||||
self.micIconView.hidden = YES;
|
||||
self.titleLabel.hidden = YES;
|
||||
self.recordingIconView.hidden = NO;
|
||||
self.waveformView.alpha = 0;
|
||||
[self.waveformView stopAnimation];
|
||||
break;
|
||||
|
||||
case KBAiRecordButtonStateDisabled:
|
||||
|
||||
28
keyBoard/Class/AiTalk/V/KBChatLimitPopView.h
Normal file
28
keyBoard/Class/AiTalk/V/KBChatLimitPopView.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// KBChatLimitPopView.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/1/27.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class KBChatLimitPopView;
|
||||
|
||||
@protocol KBChatLimitPopViewDelegate <NSObject>
|
||||
@optional
|
||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view;
|
||||
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view;
|
||||
@end
|
||||
|
||||
/// 聊天次数用尽提示弹窗内容视图(配合 LSTPopView 使用)
|
||||
@interface KBChatLimitPopView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBChatLimitPopViewDelegate> delegate;
|
||||
@property (nonatomic, copy) NSString *message;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
157
keyBoard/Class/AiTalk/V/KBChatLimitPopView.m
Normal file
157
keyBoard/Class/AiTalk/V/KBChatLimitPopView.m
Normal file
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// KBChatLimitPopView.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/1/27.
|
||||
//
|
||||
|
||||
#import "KBChatLimitPopView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface KBChatLimitPopView ()
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *messageLabel;
|
||||
@property (nonatomic, strong) UIButton *cancelButton;
|
||||
@property (nonatomic, strong) UIButton *rechargeButton;
|
||||
@property (nonatomic, strong) UIView *buttonDivider;
|
||||
@end
|
||||
|
||||
@implementation KBChatLimitPopView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.layer.cornerRadius = 16.0;
|
||||
self.layer.masksToBounds = YES;
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.titleLabel];
|
||||
[self addSubview:self.messageLabel];
|
||||
[self addSubview:self.buttonDivider];
|
||||
[self addSubview:self.cancelButton];
|
||||
[self addSubview:self.rechargeButton];
|
||||
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self).offset(20);
|
||||
make.left.equalTo(self).offset(20);
|
||||
make.right.equalTo(self).offset(-20);
|
||||
}];
|
||||
|
||||
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(8);
|
||||
make.left.equalTo(self).offset(20);
|
||||
make.right.equalTo(self).offset(-20);
|
||||
}];
|
||||
|
||||
[self.buttonDivider mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(1);
|
||||
make.top.greaterThanOrEqualTo(self.messageLabel.mas_bottom).offset(16);
|
||||
make.bottom.equalTo(self).offset(-48);
|
||||
}];
|
||||
|
||||
[self.cancelButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.bottom.equalTo(self);
|
||||
make.top.equalTo(self.buttonDivider.mas_bottom);
|
||||
make.right.equalTo(self.mas_centerX);
|
||||
}];
|
||||
|
||||
[self.rechargeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.bottom.equalTo(self);
|
||||
make.top.equalTo(self.buttonDivider.mas_bottom);
|
||||
make.left.equalTo(self.mas_centerX);
|
||||
}];
|
||||
|
||||
UIView *verticalLine = [[UIView alloc] init];
|
||||
verticalLine.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
[self addSubview:verticalLine];
|
||||
[verticalLine mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.buttonDivider.mas_bottom);
|
||||
make.bottom.equalTo(self);
|
||||
make.width.mas_equalTo(1);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onTapCancel {
|
||||
if ([self.delegate respondsToSelector:@selector(chatLimitPopViewDidTapCancel:)]) {
|
||||
[self.delegate chatLimitPopViewDidTapCancel:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onTapRecharge {
|
||||
if ([self.delegate respondsToSelector:@selector(chatLimitPopViewDidTapRecharge:)]) {
|
||||
[self.delegate chatLimitPopViewDidTapRecharge:self];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Setter
|
||||
|
||||
- (void)setMessage:(NSString *)message {
|
||||
_message = [message copy];
|
||||
self.messageLabel.text = _message.length > 0 ? _message : @"";
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = KBLocalized(@"提示");
|
||||
_titleLabel.font = [UIFont boldSystemFontOfSize:18];
|
||||
_titleLabel.textColor = [UIColor blackColor];
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)messageLabel {
|
||||
if (!_messageLabel) {
|
||||
_messageLabel = [[UILabel alloc] init];
|
||||
_messageLabel.font = [UIFont systemFontOfSize:14];
|
||||
_messageLabel.textColor = [UIColor colorWithWhite:0.2 alpha:1.0];
|
||||
_messageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_messageLabel.numberOfLines = 0;
|
||||
}
|
||||
return _messageLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)cancelButton {
|
||||
if (!_cancelButton) {
|
||||
_cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_cancelButton setTitle:KBLocalized(@"取消") forState:UIControlStateNormal];
|
||||
_cancelButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
[_cancelButton setTitleColor:[UIColor colorWithWhite:0.2 alpha:1.0] forState:UIControlStateNormal];
|
||||
[_cancelButton addTarget:self action:@selector(onTapCancel) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _cancelButton;
|
||||
}
|
||||
|
||||
- (UIButton *)rechargeButton {
|
||||
if (!_rechargeButton) {
|
||||
_rechargeButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_rechargeButton setTitle:KBLocalized(@"去充值") forState:UIControlStateNormal];
|
||||
_rechargeButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
[_rechargeButton setTitleColor:[UIColor colorWithRed:0.28 green:0.45 blue:0.94 alpha:1.0] forState:UIControlStateNormal];
|
||||
[_rechargeButton addTarget:self action:@selector(onTapRecharge) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _rechargeButton;
|
||||
}
|
||||
|
||||
- (UIView *)buttonDivider {
|
||||
if (!_buttonDivider) {
|
||||
_buttonDivider = [[UIView alloc] init];
|
||||
_buttonDivider.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
}
|
||||
return _buttonDivider;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -57,6 +57,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 重置无更多数据状态
|
||||
- (void)resetNoMoreData;
|
||||
|
||||
/// 更新底部内容 inset(用于避开输入栏/键盘)
|
||||
- (void)updateContentBottomInset:(CGFloat)bottomInset;
|
||||
|
||||
/// 添加自定义消息(可用于历史消息或打字机)
|
||||
- (void)addMessage:(KBAiChatMessage *)message
|
||||
autoScroll:(BOOL)autoScroll;
|
||||
|
||||
@@ -30,6 +30,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
@property (nonatomic, strong) NSIndexPath *playingCellIndexPath;
|
||||
@property (nonatomic, strong) AiVM *aiVM;
|
||||
@property (nonatomic, assign) BOOL hasMoreData;
|
||||
@property (nonatomic, assign) CGFloat contentBottomInset;
|
||||
|
||||
@end
|
||||
|
||||
@@ -78,11 +79,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
|
||||
// 布局
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// make.edges.equalTo(self);
|
||||
make.top.left.right.equalTo(self);
|
||||
make.bottom.equalTo(self).offset(-KB_TABBAR_HEIGHT - 40 - 10);
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
self.contentBottomInset = KB_TABBAR_HEIGHT + 40 + 10;
|
||||
[self updateContentBottomInset:self.contentBottomInset];
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
@@ -207,6 +209,14 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
[self updateFooterVisibility];
|
||||
}
|
||||
|
||||
- (void)updateContentBottomInset:(CGFloat)bottomInset {
|
||||
self.contentBottomInset = bottomInset;
|
||||
UIEdgeInsets insets = self.tableView.contentInset;
|
||||
insets.bottom = bottomInset;
|
||||
self.tableView.contentInset = insets;
|
||||
self.tableView.scrollIndicatorInsets = insets;
|
||||
}
|
||||
|
||||
- (void)addMessage:(KBAiChatMessage *)message
|
||||
autoScroll:(BOOL)autoScroll {
|
||||
if (!message) {
|
||||
|
||||
@@ -26,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)appendAssistantMessage:(NSString *)text
|
||||
audioId:(nullable NSString *)audioId;
|
||||
|
||||
/// 更新聊天列表底部 inset
|
||||
- (void)updateChatViewBottomInset:(CGFloat)bottomInset;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -288,6 +288,10 @@
|
||||
[self.chatView addMessage:message autoScroll:YES];
|
||||
}
|
||||
|
||||
- (void)updateChatViewBottomInset:(CGFloat)bottomInset {
|
||||
[self.chatView updateContentBottomInset:bottomInset];
|
||||
}
|
||||
|
||||
#pragma mark - KBChatTableViewDelegate
|
||||
|
||||
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
|
||||
|
||||
@@ -27,6 +27,18 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBVoiceInputBarMode) {
|
||||
KBVoiceInputBarModeText,
|
||||
KBVoiceInputBarModeVoice
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBVoiceInputBarState) {
|
||||
KBVoiceInputBarStateText,
|
||||
KBVoiceInputBarStateVoice,
|
||||
KBVoiceInputBarStateRecording,
|
||||
KBVoiceInputBarStateCancel
|
||||
};
|
||||
|
||||
/// 底部语音输入栏
|
||||
/// 包含:毛玻璃背景 + 录音按钮
|
||||
@interface KBVoiceInputBar : UIView
|
||||
@@ -37,6 +49,12 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 状态文本(显示在按钮上方)
|
||||
@property (nonatomic, copy) NSString *statusText;
|
||||
|
||||
/// 输入模式(文字/语音)
|
||||
@property (nonatomic, assign) KBVoiceInputBarMode inputMode;
|
||||
|
||||
/// 输入状态(文字/语音/录音/取消)
|
||||
@property (nonatomic, assign) KBVoiceInputBarState inputState;
|
||||
|
||||
/// 是否启用(禁用时按钮不可点击)
|
||||
@property (nonatomic, assign) BOOL enabled;
|
||||
|
||||
|
||||
@@ -11,18 +11,37 @@
|
||||
|
||||
@interface KBVoiceInputBar () <KBAiRecordButtonDelegate>
|
||||
|
||||
/// 毛玻璃背景容器
|
||||
@property (nonatomic, strong) UIView *backgroundView;
|
||||
|
||||
/// 毛玻璃效果
|
||||
@property (nonatomic, strong) UIVisualEffectView *blurEffectView;
|
||||
|
||||
/// 状态标签
|
||||
@property (nonatomic, strong) UILabel *statusLabel;
|
||||
|
||||
/// 录音按钮
|
||||
@property (nonatomic, strong) KBAiRecordButton *recordButton;
|
||||
|
||||
/// 输入区域容器
|
||||
@property (nonatomic, strong) UIView *inputContainer;
|
||||
|
||||
/// 文字输入视图
|
||||
@property (nonatomic, strong) UIView *textInputView;
|
||||
@property (nonatomic, strong) UIButton *textCenterButton;
|
||||
|
||||
/// 语音输入视图
|
||||
@property (nonatomic, strong) UIView *voiceInputView;
|
||||
@property (nonatomic, strong) UILabel *voiceCenterLabel;
|
||||
|
||||
/// 左侧切换按钮(麦克风/键盘共用)
|
||||
@property (nonatomic, strong) UIButton *toggleIconButton;
|
||||
|
||||
/// 录音中视图
|
||||
@property (nonatomic, strong) UIView *recordingView;
|
||||
@property (nonatomic, strong) UIImageView *recordingCenterIconView;
|
||||
|
||||
/// 取消视图
|
||||
@property (nonatomic, strong) UIView *cancelView;
|
||||
@property (nonatomic, strong) UILabel *cancelLabel;
|
||||
|
||||
/// 隐藏输入框(用于弹起键盘)
|
||||
@property (nonatomic, strong) UITextField *hiddenTextField;
|
||||
|
||||
/// 是否正在录音
|
||||
@property (nonatomic, assign) BOOL isRecording;
|
||||
|
||||
@@ -52,30 +71,8 @@
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
self.enabled = YES;
|
||||
self.isRecording = NO;
|
||||
|
||||
// 毛玻璃背景容器
|
||||
[self addSubview:self.backgroundView];
|
||||
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
// 毛玻璃效果
|
||||
[self.backgroundView addSubview:self.blurEffectView];
|
||||
[self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.backgroundView);
|
||||
}];
|
||||
|
||||
// 为 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.inputMode = KBVoiceInputBarModeVoice;
|
||||
self.inputState = KBVoiceInputBarStateText;
|
||||
|
||||
// 状态标签
|
||||
[self addSubview:self.statusLabel];
|
||||
@@ -86,24 +83,92 @@
|
||||
make.height.mas_equalTo(20);
|
||||
}];
|
||||
|
||||
// 录音按钮
|
||||
[self addSubview:self.recordButton];
|
||||
[self.recordButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// 输入区域容器
|
||||
[self addSubview:self.inputContainer];
|
||||
[self.inputContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.statusLabel.mas_bottom).offset(12);
|
||||
make.left.equalTo(self).offset(20);
|
||||
make.right.equalTo(self).offset(-20);
|
||||
make.height.mas_equalTo(50);
|
||||
make.bottom.lessThanOrEqualTo(self).offset(-16);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
UILongPressGestureRecognizer *longPress =
|
||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleVoiceLongPress:)];
|
||||
longPress.minimumPressDuration = 0.05;
|
||||
longPress.cancelsTouchesInView = NO;
|
||||
[self.inputContainer addGestureRecognizer:longPress];
|
||||
|
||||
// 更新 mask 的 frame
|
||||
if (self.blurEffectView.layer.mask) {
|
||||
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
|
||||
}
|
||||
// 文字输入视图
|
||||
[self.inputContainer addSubview:self.textInputView];
|
||||
[self.textInputView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.inputContainer);
|
||||
}];
|
||||
|
||||
[self.inputContainer addSubview:self.toggleIconButton];
|
||||
[self.toggleIconButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.textInputView).offset(16);
|
||||
make.centerY.equalTo(self.textInputView);
|
||||
make.width.height.mas_equalTo(24);
|
||||
}];
|
||||
|
||||
[self.textInputView addSubview:self.textCenterButton];
|
||||
[self.textCenterButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.toggleIconButton.mas_right).offset(12);
|
||||
make.right.equalTo(self.textInputView).offset(-16);
|
||||
make.centerY.equalTo(self.textInputView);
|
||||
make.height.mas_equalTo(30);
|
||||
}];
|
||||
|
||||
// 语音输入视图
|
||||
[self.inputContainer addSubview:self.voiceInputView];
|
||||
[self.voiceInputView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.inputContainer);
|
||||
}];
|
||||
|
||||
[self.voiceInputView addSubview:self.voiceCenterLabel];
|
||||
[self.voiceCenterLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.toggleIconButton.mas_right).offset(12);
|
||||
make.right.equalTo(self.voiceInputView).offset(-16);
|
||||
make.centerY.equalTo(self.voiceInputView);
|
||||
}];
|
||||
|
||||
// 录音中视图
|
||||
[self.inputContainer addSubview:self.recordingView];
|
||||
[self.recordingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.inputContainer);
|
||||
}];
|
||||
|
||||
[self.recordingView addSubview:self.recordingCenterIconView];
|
||||
[self.recordingCenterIconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self.recordingView);
|
||||
make.width.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 取消视图
|
||||
[self.inputContainer addSubview:self.cancelView];
|
||||
[self.cancelView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.inputContainer);
|
||||
}];
|
||||
|
||||
[self.cancelView addSubview:self.cancelLabel];
|
||||
[self.cancelLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self.cancelView);
|
||||
}];
|
||||
|
||||
// 隐藏输入框(仅用于弹起键盘)
|
||||
[self addSubview:self.hiddenTextField];
|
||||
[self.hiddenTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.height.mas_equalTo(1);
|
||||
make.left.top.equalTo(self);
|
||||
}];
|
||||
|
||||
// 隐藏旧的录音按钮和状态标签
|
||||
self.statusLabel.hidden = YES;
|
||||
|
||||
// 统一设置初始状态
|
||||
self.inputState = KBVoiceInputBarStateText;
|
||||
}
|
||||
|
||||
#pragma mark - Setter
|
||||
@@ -111,17 +176,55 @@
|
||||
- (void)setStatusText:(NSString *)statusText {
|
||||
_statusText = [statusText copy];
|
||||
self.statusLabel.text = statusText;
|
||||
[self updateCenterTextIfNeeded];
|
||||
}
|
||||
|
||||
- (void)setEnabled:(BOOL)enabled {
|
||||
_enabled = enabled;
|
||||
self.recordButton.userInteractionEnabled = enabled;
|
||||
self.recordButton.alpha = enabled ? 1.0 : 0.5;
|
||||
self.inputContainer.userInteractionEnabled = enabled;
|
||||
self.inputContainer.alpha = enabled ? 1.0 : 0.5;
|
||||
}
|
||||
|
||||
- (void)setRecording:(BOOL)recording {
|
||||
_isRecording = recording;
|
||||
self.recordButton.state = recording ? KBAiRecordButtonStateRecording : KBAiRecordButtonStateNormal;
|
||||
if (recording) {
|
||||
self.inputState = KBVoiceInputBarStateRecording;
|
||||
} else if (self.inputState == KBVoiceInputBarStateRecording) {
|
||||
self.inputState = KBVoiceInputBarStateVoice;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputMode:(KBVoiceInputBarMode)inputMode {
|
||||
_inputMode = inputMode;
|
||||
if (inputMode == KBVoiceInputBarModeText) {
|
||||
[self.toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"]
|
||||
forState:UIControlStateNormal];
|
||||
} else {
|
||||
[self.toggleIconButton setImage:[UIImage imageNamed:@"ai_jianpan_icon"]
|
||||
forState:UIControlStateNormal];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputState:(KBVoiceInputBarState)inputState {
|
||||
_inputState = inputState;
|
||||
self.textInputView.hidden = (inputState != KBVoiceInputBarStateText);
|
||||
self.voiceInputView.hidden = (inputState != KBVoiceInputBarStateVoice);
|
||||
self.recordingView.hidden = (inputState != KBVoiceInputBarStateRecording);
|
||||
self.cancelView.hidden = (inputState != KBVoiceInputBarStateCancel);
|
||||
self.toggleIconButton.hidden = (inputState == KBVoiceInputBarStateRecording ||
|
||||
inputState == KBVoiceInputBarStateCancel);
|
||||
if (inputState == KBVoiceInputBarStateText) {
|
||||
self.inputMode = KBVoiceInputBarModeText;
|
||||
} else if (inputState == KBVoiceInputBarStateVoice) {
|
||||
self.inputMode = KBVoiceInputBarModeVoice;
|
||||
}
|
||||
if (!self.toggleIconButton.hidden) {
|
||||
[self.inputContainer bringSubviewToFront:self.toggleIconButton];
|
||||
}
|
||||
[self updateCenterTextIfNeeded];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
@@ -162,22 +265,6 @@
|
||||
|
||||
#pragma mark - Lazy Load
|
||||
|
||||
- (UIView *)backgroundView {
|
||||
if (!_backgroundView) {
|
||||
_backgroundView = [[UIView alloc] init];
|
||||
_backgroundView.clipsToBounds = YES;
|
||||
}
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (UIVisualEffectView *)blurEffectView {
|
||||
if (!_blurEffectView) {
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
_blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||||
}
|
||||
return _blurEffectView;
|
||||
}
|
||||
|
||||
- (UILabel *)statusLabel {
|
||||
if (!_statusLabel) {
|
||||
_statusLabel = [[UILabel alloc] init];
|
||||
@@ -195,8 +282,198 @@
|
||||
_recordButton.delegate = self;
|
||||
_recordButton.normalTitle = @"按住说话";
|
||||
_recordButton.recordingTitle = @"松开结束";
|
||||
_recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"];
|
||||
_recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"];
|
||||
_recordButton.hidden = YES;
|
||||
}
|
||||
return _recordButton;
|
||||
}
|
||||
|
||||
- (UIView *)inputContainer {
|
||||
if (!_inputContainer) {
|
||||
_inputContainer = [[UIView alloc] init];
|
||||
_inputContainer.clipsToBounds = YES;
|
||||
_inputContainer.layer.cornerRadius = 25;
|
||||
_inputContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _inputContainer;
|
||||
}
|
||||
|
||||
- (UIView *)textInputView {
|
||||
if (!_textInputView) {
|
||||
_textInputView = [[UIView alloc] init];
|
||||
_textInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
}
|
||||
return _textInputView;
|
||||
}
|
||||
|
||||
- (UIButton *)textCenterButton {
|
||||
if (!_textCenterButton) {
|
||||
_textCenterButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
||||
_textCenterButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
[_textCenterButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_textCenterButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
|
||||
[_textCenterButton addTarget:self
|
||||
action:@selector(handleTextCenterTap)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _textCenterButton;
|
||||
}
|
||||
|
||||
- (UIView *)voiceInputView {
|
||||
if (!_voiceInputView) {
|
||||
_voiceInputView = [[UIView alloc] init];
|
||||
_voiceInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
}
|
||||
return _voiceInputView;
|
||||
}
|
||||
|
||||
- (UILabel *)voiceCenterLabel {
|
||||
if (!_voiceCenterLabel) {
|
||||
_voiceCenterLabel = [[UILabel alloc] init];
|
||||
_voiceCenterLabel.text = @"按住说话";
|
||||
_voiceCenterLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_voiceCenterLabel.textColor = [UIColor whiteColor];
|
||||
_voiceCenterLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _voiceCenterLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)toggleIconButton {
|
||||
if (!_toggleIconButton) {
|
||||
_toggleIconButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"]
|
||||
forState:UIControlStateNormal];
|
||||
[_toggleIconButton addTarget:self
|
||||
action:@selector(handleToggleIconTap)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
_toggleIconButton.exclusiveTouch = YES;
|
||||
}
|
||||
return _toggleIconButton;
|
||||
}
|
||||
|
||||
- (UIView *)recordingView {
|
||||
if (!_recordingView) {
|
||||
_recordingView = [[UIView alloc] init];
|
||||
_recordingView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6];
|
||||
}
|
||||
return _recordingView;
|
||||
}
|
||||
|
||||
- (UIImageView *)recordingCenterIconView {
|
||||
if (!_recordingCenterIconView) {
|
||||
_recordingCenterIconView = [[UIImageView alloc] init];
|
||||
_recordingCenterIconView.image = [UIImage imageNamed:@"ai_luyining_icon"];
|
||||
_recordingCenterIconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
return _recordingCenterIconView;
|
||||
}
|
||||
|
||||
- (UIView *)cancelView {
|
||||
if (!_cancelView) {
|
||||
_cancelView = [[UIView alloc] init];
|
||||
_cancelView.backgroundColor = [UIColor colorWithRed:0.75 green:0.3 blue:0.3 alpha:1.0];
|
||||
}
|
||||
return _cancelView;
|
||||
}
|
||||
|
||||
- (UILabel *)cancelLabel {
|
||||
if (!_cancelLabel) {
|
||||
_cancelLabel = [[UILabel alloc] init];
|
||||
_cancelLabel.text = @"Release To Cancel";
|
||||
_cancelLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_cancelLabel.textColor = [UIColor whiteColor];
|
||||
}
|
||||
return _cancelLabel;
|
||||
}
|
||||
|
||||
- (UITextField *)hiddenTextField {
|
||||
if (!_hiddenTextField) {
|
||||
_hiddenTextField = [[UITextField alloc] init];
|
||||
_hiddenTextField.hidden = YES;
|
||||
}
|
||||
return _hiddenTextField;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)handleToggleIconTap {
|
||||
if (self.inputState == KBVoiceInputBarStateText) {
|
||||
self.inputState = KBVoiceInputBarStateVoice;
|
||||
[self endEditing:YES];
|
||||
} else {
|
||||
self.inputState = KBVoiceInputBarStateText;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTextCenterTap {
|
||||
self.inputState = KBVoiceInputBarStateText;
|
||||
[self.hiddenTextField becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)handleVoiceLongPress:(UILongPressGestureRecognizer *)gesture {
|
||||
if (self.inputState != KBVoiceInputBarStateVoice &&
|
||||
self.inputState != KBVoiceInputBarStateRecording &&
|
||||
self.inputState != KBVoiceInputBarStateCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint location = [gesture locationInView:self.inputContainer];
|
||||
BOOL isInside = CGRectContainsPoint(self.inputContainer.bounds, location);
|
||||
CGPoint iconPoint = [gesture locationInView:self.toggleIconButton];
|
||||
BOOL isOnToggleIcon = CGRectContainsPoint(self.toggleIconButton.bounds, iconPoint);
|
||||
if (isOnToggleIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (gesture.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
if (self.inputState != KBVoiceInputBarStateVoice) {
|
||||
return;
|
||||
}
|
||||
self.inputState = KBVoiceInputBarStateRecording;
|
||||
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidBeginRecording:)]) {
|
||||
[self.delegate voiceInputBarDidBeginRecording:self];
|
||||
}
|
||||
} break;
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
if (isInside) {
|
||||
self.inputState = KBVoiceInputBarStateRecording;
|
||||
} else {
|
||||
self.inputState = KBVoiceInputBarStateCancel;
|
||||
}
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded: {
|
||||
if (isInside) {
|
||||
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidEndRecording:)]) {
|
||||
[self.delegate voiceInputBarDidEndRecording:self];
|
||||
}
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) {
|
||||
[self.delegate voiceInputBarDidCancelRecording:self];
|
||||
}
|
||||
}
|
||||
self.inputState = KBVoiceInputBarStateVoice;
|
||||
} break;
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) {
|
||||
[self.delegate voiceInputBarDidCancelRecording:self];
|
||||
}
|
||||
self.inputState = KBVoiceInputBarStateVoice;
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateCenterTextIfNeeded {
|
||||
if (self.inputState == KBVoiceInputBarStateText) {
|
||||
[self.textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
||||
} else if (self.inputState == KBVoiceInputBarStateVoice) {
|
||||
self.voiceCenterLabel.text = @"按住说话";
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,15 +13,30 @@
|
||||
#import "KBVoiceToTextManager.h"
|
||||
#import "AiVM.h"
|
||||
#import "KBHUD.h"
|
||||
#import "KBChatLimitPopView.h"
|
||||
#import "KBVipPay.h"
|
||||
#import "KBUserSessionManager.h"
|
||||
#import "LSTPopView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate>
|
||||
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate>
|
||||
|
||||
/// 人设列表容器
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
|
||||
/// 底部语音输入栏
|
||||
@property (nonatomic, strong) KBVoiceInputBar *voiceInputBar;
|
||||
@property (nonatomic, strong) MASConstraint *voiceInputBarBottomConstraint;
|
||||
@property (nonatomic, assign) CGFloat voiceInputBarHeight;
|
||||
@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing;
|
||||
@property (nonatomic, assign) CGFloat currentKeyboardHeight;
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
|
||||
@property (nonatomic, weak) LSTPopView *chatLimitPopView;
|
||||
|
||||
/// 底部毛玻璃背景
|
||||
@property (nonatomic, strong) UIView *bottomBackgroundView;
|
||||
@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView;
|
||||
@property (nonatomic, strong) CAGradientLayer *bottomMaskLayer;
|
||||
|
||||
/// 语音转写管理器
|
||||
@property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager;
|
||||
@@ -72,23 +87,47 @@
|
||||
[self setupUI];
|
||||
[self setupVoiceToTextManager];
|
||||
[self setupVoiceRecordManager];
|
||||
[self setupKeyboardNotifications];
|
||||
[self setupKeyboardDismissGesture];
|
||||
[self loadPersonas];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
if (self.bottomMaskLayer) {
|
||||
self.bottomMaskLayer.frame = self.bottomBlurEffectView.bounds;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 1:控件初始化
|
||||
|
||||
- (void)setupUI {
|
||||
self.voiceInputBarHeight = 150.0;
|
||||
self.baseInputBarBottomSpacing = 20.0;
|
||||
[self.view addSubview:self.collectionView];
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 底部毛玻璃背景
|
||||
[self.view addSubview:self.bottomBackgroundView];
|
||||
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
|
||||
[self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
// self.bottomBackgroundBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
||||
make.bottom.equalTo(self.view);
|
||||
make.height.mas_equalTo(self.voiceInputBarHeight);
|
||||
}];
|
||||
[self.bottomBlurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.bottomBackgroundView);
|
||||
}];
|
||||
|
||||
// 底部语音输入栏
|
||||
[self.view addSubview:self.voiceInputBar];
|
||||
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
make.bottom.equalTo(self.view).offset(-20);
|
||||
make.height.mas_equalTo(150); // 根据实际需要调整高度
|
||||
self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
||||
make.height.mas_equalTo(self.voiceInputBarHeight); // 根据实际需要调整高度
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -206,6 +245,7 @@
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
||||
cell.persona = self.personas[indexPath.item];
|
||||
[self updateChatViewBottomInset];
|
||||
|
||||
// 标记为已预加载
|
||||
[self.preloadedIndexes addObject:@(indexPath.item)];
|
||||
@@ -244,6 +284,8 @@
|
||||
if (currentPage < self.personas.count) {
|
||||
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
||||
}
|
||||
|
||||
[self updateChatViewBottomInset];
|
||||
}
|
||||
|
||||
#pragma mark - 4:语音转写
|
||||
@@ -262,6 +304,61 @@
|
||||
self.voiceRecordManager.minRecordDuration = 1.0;
|
||||
}
|
||||
|
||||
#pragma mark - 6:键盘监听
|
||||
|
||||
- (void)setupKeyboardNotifications {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleKeyboardWillChangeFrame:)
|
||||
name:UIKeyboardWillChangeFrameNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)handleKeyboardWillChangeFrame:(NSNotification *)notification {
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16);
|
||||
|
||||
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
||||
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
||||
self.currentKeyboardHeight = keyboardHeight;
|
||||
|
||||
CGFloat bottomSpacing = (keyboardHeight > 0.0) ? (keyboardHeight + 8.0) : self.baseInputBarBottomSpacing;
|
||||
[self.voiceInputBarBottomConstraint setOffset:-bottomSpacing];
|
||||
[self updateChatViewBottomInset];
|
||||
|
||||
[UIView animateWithDuration:duration
|
||||
delay:0
|
||||
options:options
|
||||
animations:^{
|
||||
[self.view layoutIfNeeded];
|
||||
}
|
||||
completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - 7:键盘收起
|
||||
|
||||
- (void)setupKeyboardDismissGesture {
|
||||
self.dismissKeyboardTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleBackgroundTap)];
|
||||
self.dismissKeyboardTap.cancelsTouchesInView = NO;
|
||||
self.dismissKeyboardTap.delegate = self;
|
||||
[self.view addGestureRecognizer:self.dismissKeyboardTap];
|
||||
}
|
||||
|
||||
- (void)handleBackgroundTap {
|
||||
[self.view endEditing:YES];
|
||||
}
|
||||
|
||||
#pragma mark - UIGestureRecognizerDelegate
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
||||
if ([touch.view isDescendantOfView:self.voiceInputBar]) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSInteger)currentCompanionId {
|
||||
if (self.personas.count == 0) {
|
||||
return 0;
|
||||
@@ -302,6 +399,42 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)updateChatViewBottomInset {
|
||||
CGFloat bottomSpacing = (self.currentKeyboardHeight > 0.0) ? (self.currentKeyboardHeight + 8.0) : self.baseInputBarBottomSpacing;
|
||||
CGFloat bottomInset = self.voiceInputBarHeight + bottomSpacing;
|
||||
|
||||
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
||||
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||
if (cell) {
|
||||
[cell updateChatViewBottomInset:bottomInset];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showChatLimitPopWithMessage:(NSString *)message {
|
||||
if (self.chatLimitPopView) {
|
||||
[self.chatLimitPopView dismiss];
|
||||
}
|
||||
|
||||
CGFloat width = KB_SCREEN_WIDTH - 60;
|
||||
KBChatLimitPopView *content = [[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)];
|
||||
content.message = message;
|
||||
content.delegate = self;
|
||||
|
||||
LSTPopView *pop = [LSTPopView initWithCustomView:content
|
||||
parentView:nil
|
||||
popStyle:LSTPopStyleFade
|
||||
dismissStyle:LSTDismissStyleFade];
|
||||
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
||||
pop.hemStyle = LSTHemStyleCenter;
|
||||
pop.isClickBgDismiss = YES;
|
||||
pop.isAvoidKeyboard = NO;
|
||||
self.chatLimitPopView = pop;
|
||||
[pop pop];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Load
|
||||
|
||||
- (UICollectionView *)collectionView {
|
||||
@@ -335,59 +468,59 @@
|
||||
return _voiceInputBar;
|
||||
}
|
||||
|
||||
#pragma mark - KBChatLimitPopViewDelegate
|
||||
|
||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||||
[self.chatLimitPopView dismiss];
|
||||
}
|
||||
|
||||
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
||||
[self.chatLimitPopView dismiss];
|
||||
if (![KBUserSessionManager shared].isLoggedIn) {
|
||||
[[KBUserSessionManager shared] goLoginVC];
|
||||
return;
|
||||
}
|
||||
KBVipPay *vc = [[KBVipPay alloc] init];
|
||||
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
||||
}
|
||||
|
||||
- (UIView *)bottomBackgroundView {
|
||||
if (!_bottomBackgroundView) {
|
||||
_bottomBackgroundView = [[UIView alloc] init];
|
||||
_bottomBackgroundView.clipsToBounds = YES;
|
||||
}
|
||||
return _bottomBackgroundView;
|
||||
}
|
||||
|
||||
- (UIVisualEffectView *)bottomBlurEffectView {
|
||||
if (!_bottomBlurEffectView) {
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
_bottomBlurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||||
_bottomBlurEffectView.layer.mask = self.bottomMaskLayer;
|
||||
}
|
||||
return _bottomBlurEffectView;
|
||||
}
|
||||
|
||||
- (CAGradientLayer *)bottomMaskLayer {
|
||||
if (!_bottomMaskLayer) {
|
||||
_bottomMaskLayer = [CAGradientLayer layer];
|
||||
_bottomMaskLayer.startPoint = CGPointMake(0.5, 1);
|
||||
_bottomMaskLayer.endPoint = CGPointMake(0.5, 0);
|
||||
_bottomMaskLayer.colors = @[
|
||||
(__bridge id)[UIColor whiteColor].CGColor,
|
||||
(__bridge id)[UIColor whiteColor].CGColor,
|
||||
(__bridge id)[UIColor clearColor].CGColor
|
||||
];
|
||||
_bottomMaskLayer.locations = @[@(0.0), @(0.5), @(1.0)];
|
||||
}
|
||||
return _bottomMaskLayer;
|
||||
}
|
||||
|
||||
#pragma mark - KBVoiceToTextManagerDelegate
|
||||
|
||||
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
||||
didReceiveFinalText:(NSString *)text {
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text);
|
||||
|
||||
NSInteger companionId = [self currentCompanionId];
|
||||
if (companionId <= 0) {
|
||||
NSLog(@"[KBAIHomeVC] companionId 无效,取消请求");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
[currentCell appendUserMessage:text];
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM requestChatMessageWithContent:text
|
||||
companionId:companionId
|
||||
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.data) {
|
||||
NSLog(@"[KBAIHomeVC] 聊天响应为空");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
||||
NSString *audioId = response.data.audioId;
|
||||
if (aiResponse.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] AI 回复为空");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell appendAssistantMessage:aiResponse audioId:audioId];
|
||||
}
|
||||
});
|
||||
}];
|
||||
[self handleTranscribedText:text];
|
||||
}
|
||||
|
||||
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
||||
@@ -417,6 +550,32 @@
|
||||
error:nil];
|
||||
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
||||
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM transcribeAudioFileAtURL:fileURL
|
||||
completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription);
|
||||
[KBHUD showError:KBLocalized(@"语音转文字失败,请重试")];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *transcript = response.data.transcript ?: @"";
|
||||
if (transcript.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
|
||||
[KBHUD showError:KBLocalized(@"未识别到语音内容")];
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf handleTranscribedText:transcript];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager {
|
||||
@@ -429,4 +588,72 @@
|
||||
NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription);
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)handleTranscribedText:(NSString *)text {
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text);
|
||||
|
||||
NSInteger companionId = [self currentCompanionId];
|
||||
if (companionId <= 0) {
|
||||
NSLog(@"[KBAIHomeVC] companionId 无效,取消请求");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
[currentCell appendUserMessage:text];
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM requestChatMessageWithContent:text
|
||||
companionId:companionId
|
||||
completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// if (error) {
|
||||
// NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (response.code == 50030) {
|
||||
NSString *message = response.message ?: @"";
|
||||
[strongSelf showChatLimitPopWithMessage:message];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.data) {
|
||||
NSString *message = response.message ?: @"聊天响应为空";
|
||||
NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message);
|
||||
if (message.length > 0) {
|
||||
[KBHUD showError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
||||
NSString *audioId = response.data.audioId;
|
||||
if (aiResponse.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] AI 回复为空");
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell appendAssistantMessage:aiResponse audioId:audioId];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#import "KBChatTableView.h"
|
||||
#import "KBAiRecordButton.h"
|
||||
#import "KBHUD.h"
|
||||
#import "KBChatLimitPopView.h"
|
||||
#import "KBVipPay.h"
|
||||
#import "LSTPopView.h"
|
||||
#import "VoiceChatStreamingManager.h"
|
||||
#import "KBUserSessionManager.h"
|
||||
@@ -22,8 +24,10 @@
|
||||
@interface KBAiMainVC () <KBAiRecordButtonDelegate,
|
||||
VoiceChatStreamingManagerDelegate,
|
||||
DeepgramStreamingManagerDelegate,
|
||||
AVAudioPlayerDelegate>
|
||||
AVAudioPlayerDelegate,
|
||||
KBChatLimitPopViewDelegate>
|
||||
@property(nonatomic, weak) LSTPopView *popView;
|
||||
@property(nonatomic, weak) LSTPopView *limitPopView;
|
||||
|
||||
// UI
|
||||
@property(nonatomic, strong) KBChatTableView *chatView;
|
||||
@@ -419,6 +423,48 @@
|
||||
self.commentView = customView;
|
||||
}
|
||||
|
||||
#pragma mark - 次数用尽弹窗
|
||||
|
||||
- (void)showChatLimitPopWithMessage:(NSString *)message {
|
||||
if (self.limitPopView) {
|
||||
[self.limitPopView dismiss];
|
||||
}
|
||||
|
||||
CGFloat width = KB_SCREEN_WIDTH - 60;
|
||||
KBChatLimitPopView *content =
|
||||
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)];
|
||||
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;
|
||||
}
|
||||
KBVipPay *vc = [[KBVipPay alloc] init];
|
||||
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
||||
}
|
||||
|
||||
#pragma mark - UI Updates
|
||||
|
||||
- (void)updateStatusForState:(ConversationState)state {
|
||||
@@ -685,6 +731,18 @@
|
||||
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 ?: @"";
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ typedef void (^AiVMSyncCompletion)(KBAiSyncResponse *_Nullable response,
|
||||
@interface KBAiMessageResponse : NSObject
|
||||
@property(nonatomic, assign) NSInteger code;
|
||||
@property(nonatomic, strong, nullable) KBAiMessageData *data;
|
||||
@property(nonatomic, copy, nullable) NSString *message;
|
||||
@end
|
||||
|
||||
typedef void (^AiVMMessageCompletion)(KBAiMessageResponse *_Nullable response,
|
||||
@@ -47,6 +48,22 @@ typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL,
|
||||
typedef void (^AiVMUploadAudioCompletion)(NSString *_Nullable fileURL,
|
||||
NSError *_Nullable error);
|
||||
|
||||
@interface KBAiSpeechTranscribeData : NSObject
|
||||
@property(nonatomic, copy, nullable) NSString *transcript;
|
||||
@property(nonatomic, assign) double confidence;
|
||||
@property(nonatomic, assign) double duration;
|
||||
@property(nonatomic, copy, nullable) NSString *detectedLanguage;
|
||||
@end
|
||||
|
||||
@interface KBAiSpeechTranscribeResponse : NSObject
|
||||
@property(nonatomic, assign) NSInteger code;
|
||||
@property(nonatomic, strong, nullable) KBAiSpeechTranscribeData *data;
|
||||
@property(nonatomic, copy, nullable) NSString *message;
|
||||
@end
|
||||
|
||||
typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nullable response,
|
||||
NSError *_Nullable error);
|
||||
|
||||
@interface AiVM : NSObject
|
||||
|
||||
- (void)syncChatWithTranscript:(NSString *)transcript
|
||||
@@ -64,6 +81,10 @@ typedef void (^AiVMUploadAudioCompletion)(NSString *_Nullable fileURL,
|
||||
- (void)uploadAudioFileAtURL:(NSURL *)fileURL
|
||||
completion:(AiVMUploadAudioCompletion)completion;
|
||||
|
||||
/// 语音转文字(multipart/form-data)
|
||||
- (void)transcribeAudioFileAtURL:(NSURL *)fileURL
|
||||
completion:(AiVMSpeechTranscribeCompletion)completion;
|
||||
|
||||
#pragma mark - 人设相关接口
|
||||
|
||||
/// 分页查询人设列表
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
@implementation KBAiMessageResponse
|
||||
@end
|
||||
|
||||
@implementation KBAiSpeechTranscribeData
|
||||
@end
|
||||
|
||||
@implementation KBAiSpeechTranscribeResponse
|
||||
@end
|
||||
|
||||
@implementation AiVM
|
||||
|
||||
- (void)syncChatWithTranscript:(NSString *)transcript
|
||||
@@ -126,15 +132,16 @@ autoShowBusinessError:NO
|
||||
completion:^(NSDictionary *_Nullable json,
|
||||
NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable error) {
|
||||
KBAiMessageResponse *model =
|
||||
[KBAiMessageResponse mj_objectWithKeyValues:json];
|
||||
if (error) {
|
||||
if (completion) {
|
||||
completion(nil, error);
|
||||
completion(model, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
KBAiMessageResponse *model =
|
||||
[KBAiMessageResponse mj_objectWithKeyValues:json];
|
||||
|
||||
id dataObj = json[@"data"];
|
||||
if (!model.data && [dataObj isKindOfClass:[NSString class]]) {
|
||||
KBAiMessageData *data = [[KBAiMessageData alloc] init];
|
||||
@@ -261,6 +268,42 @@ autoShowBusinessError:NO
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)transcribeAudioFileAtURL:(NSURL *)fileURL
|
||||
completion:(AiVMSpeechTranscribeCompletion)completion {
|
||||
if (!fileURL || !fileURL.isFileURL) {
|
||||
NSError *error = [NSError errorWithDomain:@"AiVM"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"invalid fileURL"}];
|
||||
if (completion) {
|
||||
completion(nil, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[[KBNetworkManager shared] uploadFile:API_AI_SPEECH_TRANSCRIBE
|
||||
fileURL:fileURL
|
||||
name:@"file"
|
||||
mimeType:@"audio/m4a"
|
||||
parameters:nil
|
||||
headers:nil
|
||||
completion:^(NSDictionary *_Nullable json,
|
||||
NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable error) {
|
||||
if (error) {
|
||||
if (completion) {
|
||||
completion(nil, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
KBAiSpeechTranscribeResponse *model =
|
||||
[KBAiSpeechTranscribeResponse mj_objectWithKeyValues:json];
|
||||
if (completion) {
|
||||
completion(model, nil);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - 人设相关接口
|
||||
|
||||
- (void)fetchPersonasWithPageNum:(NSInteger)pageNum
|
||||
|
||||
Reference in New Issue
Block a user