添加侧边栏

This commit is contained in:
2026-02-03 16:54:38 +08:00
parent a0923c8572
commit b9663037f5
16 changed files with 728 additions and 2 deletions

View File

@@ -230,6 +230,7 @@
04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; }; 04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; };
04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */; }; 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */; };
0F2A10032F3C0001002CA5A0 /* KBChatMessageActionPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F2A10022F3C0001002CA5A0 /* KBChatMessageActionPopView.m */; }; 0F2A10032F3C0001002CA5A0 /* KBChatMessageActionPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F2A10022F3C0001002CA5A0 /* KBChatMessageActionPopView.m */; };
0F2A10132F3C0002002CA5A0 /* KBAIPersonaSidebarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F2A10122F3C0002002CA5A0 /* KBAIPersonaSidebarView.m */; };
04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039482F236E75002CA5A0 /* KBChatTimeCell.m */; }; 04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039482F236E75002CA5A0 /* KBChatTimeCell.m */; };
04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039432F236E75002CA5A0 /* KBChatAssistantMessageCell.m */; }; 04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039432F236E75002CA5A0 /* KBChatAssistantMessageCell.m */; };
04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039452F236E75002CA5A0 /* KBChatTableView.m */; }; 04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039452F236E75002CA5A0 /* KBChatTableView.m */; };
@@ -563,6 +564,8 @@
048FFD1C2F277486005D62AE /* KBChatHistoryPageModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatHistoryPageModel.m; sourceTree = "<group>"; }; 048FFD1C2F277486005D62AE /* KBChatHistoryPageModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatHistoryPageModel.m; sourceTree = "<group>"; };
048FFD222F28A836005D62AE /* KBChatLimitPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatLimitPopView.h; sourceTree = "<group>"; }; 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatLimitPopView.h; sourceTree = "<group>"; };
048FFD232F28A836005D62AE /* KBChatLimitPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatLimitPopView.m; sourceTree = "<group>"; }; 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatLimitPopView.m; sourceTree = "<group>"; };
0F2A10112F3C0002002CA5A0 /* KBAIPersonaSidebarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIPersonaSidebarView.h; sourceTree = "<group>"; };
0F2A10122F3C0002002CA5A0 /* KBAIPersonaSidebarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIPersonaSidebarView.m; sourceTree = "<group>"; };
048FFD252F28C6CF005D62AE /* KBImagePositionButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBImagePositionButton.h; sourceTree = "<group>"; }; 048FFD252F28C6CF005D62AE /* KBImagePositionButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBImagePositionButton.h; sourceTree = "<group>"; };
048FFD262F28C6CF005D62AE /* KBImagePositionButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBImagePositionButton.m; sourceTree = "<group>"; }; 048FFD262F28C6CF005D62AE /* KBImagePositionButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBImagePositionButton.m; sourceTree = "<group>"; };
048FFD282F28E99A005D62AE /* KBCommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCommentModel.h; sourceTree = "<group>"; }; 048FFD282F28E99A005D62AE /* KBCommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCommentModel.h; sourceTree = "<group>"; };
@@ -1449,6 +1452,8 @@
children = ( children = (
048FFD222F28A836005D62AE /* KBChatLimitPopView.h */, 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */,
048FFD232F28A836005D62AE /* KBChatLimitPopView.m */, 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */,
0F2A10112F3C0002002CA5A0 /* KBAIPersonaSidebarView.h */,
0F2A10122F3C0002002CA5A0 /* KBAIPersonaSidebarView.m */,
); );
path = PopView; path = PopView;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2419,6 +2424,7 @@
046086D92F1A093400757C95 /* KBAICommentHeaderView.m in Sources */, 046086D92F1A093400757C95 /* KBAICommentHeaderView.m in Sources */,
04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */, 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */,
0F2A10032F3C0001002CA5A0 /* KBChatMessageActionPopView.m in Sources */, 0F2A10032F3C0001002CA5A0 /* KBChatMessageActionPopView.m in Sources */,
0F2A10132F3C0002002CA5A0 /* KBAIPersonaSidebarView.m in Sources */,
04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */, 04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */,
04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */, 04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */,
04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */, 04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */,

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

View File

@@ -0,0 +1,51 @@
//
// KBAIPersonaSidebarView.h
// keyBoard
//
// Created by Codex on 2026/2/3.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class KBPersonaModel;
@class KBAIPersonaSidebarView;
@protocol KBAIPersonaSidebarViewDelegate <NSObject>
@optional
/// 侧边栏请求人设数据
/// page 从 1 开始
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
requestPersonasAtPage:(NSInteger)page;
/// 选择某个人设
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
didSelectPersona:(KBPersonaModel *)persona;
@end
/// 人设侧边栏LSTPopView 内容视图)
@interface KBAIPersonaSidebarView : UIView
@property (nonatomic, weak) id<KBAIPersonaSidebarViewDelegate> delegate;
@property (nonatomic, assign) NSInteger selectedPersonaId;
@property (nonatomic, assign, readonly) NSInteger currentPage;
/// 更新人设列表
/// reset=YES 表示重置并替换数据reset=NO 表示追加
/// currentPage 为当前已加载页数(从 1 开始)
- (void)updatePersonas:(NSArray<KBPersonaModel *> *)personas
reset:(BOOL)reset
hasMore:(BOOL)hasMore
currentPage:(NSInteger)currentPage;
/// 请求数据(若为空)
- (void)requestPersonasIfNeeded;
/// 更新选中态
- (void)updateSelectedPersonaId:(NSInteger)personaId;
/// 结束加载更多
- (void)endLoadingMore;
/// 重置加载更多状态
- (void)resetLoadMore;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,417 @@
//
// KBAIPersonaSidebarView.m
// keyBoard
//
// Created by Codex on 2026/2/3.
//
#import "KBAIPersonaSidebarView.h"
#import "KBPersonaModel.h"
#import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h>
#import <MJRefresh/MJRefresh.h>
#pragma mark - Cell
@interface KBAIPersonaSidebarCell : UITableViewCell
@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *descLabel;
@property (nonatomic, strong) UIImageView *checkImageView;
@property (nonatomic, strong) UIImageView *arrowImageView;
@property (nonatomic, strong) UIView *lineView;
- (void)configureWithPersona:(KBPersonaModel *)persona selected:(BOOL)selected;
@end
@implementation KBAIPersonaSidebarCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self setupUI];
}
return self;
}
- (void)setupUI {
[self.contentView addSubview:self.avatarImageView];
[self.contentView addSubview:self.nameLabel];
[self.contentView addSubview:self.descLabel];
[self.contentView addSubview:self.checkImageView];
[self.contentView addSubview:self.arrowImageView];
[self.contentView addSubview:self.lineView];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(44);
}];
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.avatarImageView.mas_right).offset(12);
make.top.equalTo(self.avatarImageView).offset(2);
make.right.lessThanOrEqualTo(self.contentView).offset(-60);
}];
[self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.nameLabel);
make.top.equalTo(self.nameLabel.mas_bottom).offset(4);
make.right.lessThanOrEqualTo(self.contentView).offset(-60);
}];
[self.checkImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contentView);
make.right.equalTo(self.contentView).offset(-16);
make.width.height.mas_equalTo(22);
}];
[self.arrowImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contentView);
make.right.equalTo(self.contentView).offset(-18);
make.width.height.mas_equalTo(16);
}];
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.nameLabel);
make.right.equalTo(self.contentView).offset(-16);
make.bottom.equalTo(self.contentView);
make.height.mas_equalTo(0.5);
}];
}
- (void)configureWithPersona:(KBPersonaModel *)persona selected:(BOOL)selected {
if (!persona) {
return;
}
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
self.nameLabel.text = persona.name ?: @"";
NSString *desc = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
self.descLabel.text = desc ?: @"";
self.checkImageView.hidden = !selected;
self.arrowImageView.hidden = selected;
}
#pragma mark - Lazy
- (UIImageView *)avatarImageView {
if (!_avatarImageView) {
_avatarImageView = [[UIImageView alloc] init];
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
_avatarImageView.layer.cornerRadius = 22;
_avatarImageView.clipsToBounds = YES;
_avatarImageView.layer.borderWidth = 1;
_avatarImageView.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6].CGColor;
}
return _avatarImageView;
}
- (UILabel *)nameLabel {
if (!_nameLabel) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
_nameLabel.textColor = [UIColor whiteColor];
}
return _nameLabel;
}
- (UILabel *)descLabel {
if (!_descLabel) {
_descLabel = [[UILabel alloc] init];
_descLabel.font = [UIFont systemFontOfSize:12];
_descLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
_descLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
return _descLabel;
}
- (UIImageView *)checkImageView {
if (!_checkImageView) {
_checkImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ai_role_sel"]];
}
return _checkImageView;
}
- (UIImageView *)arrowImageView {
if (!_arrowImageView) {
_arrowImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"right_arrow_icon"]];
}
return _arrowImageView;
}
- (UIView *)lineView {
if (!_lineView) {
_lineView = [[UIView alloc] init];
_lineView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
}
return _lineView;
}
@end
#pragma mark - View
@interface KBAIPersonaSidebarView () <UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate>
@property (nonatomic, strong) UIVisualEffectView *blurView;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIView *searchContainer;
@property (nonatomic, strong) UIImageView *searchIconView;
@property (nonatomic, strong) UITextField *searchField;
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray<KBPersonaModel *> *personas;
@property (nonatomic, strong) NSArray<KBPersonaModel *> *displayPersonas;
@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, assign) BOOL hasMore;
@end
@implementation KBAIPersonaSidebarView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupUI];
}
return self;
}
- (void)setupUI {
self.backgroundColor = [UIColor clearColor];
[self addSubview:self.blurView];
[self addSubview:self.contentView];
[self.contentView addSubview:self.searchContainer];
[self.searchContainer addSubview:self.searchIconView];
[self.searchContainer addSubview:self.searchField];
[self.contentView addSubview:self.tableView];
[self.blurView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
[self.searchContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(14);
make.left.equalTo(self.contentView).offset(16);
make.right.equalTo(self.contentView).offset(-16);
make.height.mas_equalTo(36);
}];
[self.searchIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.searchContainer).offset(12);
make.centerY.equalTo(self.searchContainer);
make.width.height.mas_equalTo(16);
}];
[self.searchField mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.searchIconView.mas_right).offset(8);
make.right.equalTo(self.searchContainer).offset(-12);
make.centerY.equalTo(self.searchContainer);
make.height.mas_equalTo(28);
}];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.searchContainer.mas_bottom).offset(10);
make.left.right.bottom.equalTo(self.contentView);
}];
__weak typeof(self) weakSelf = self;
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (!strongSelf.hasMore) {
[strongSelf.tableView.mj_footer endRefreshingWithNoMoreData];
return;
}
strongSelf.currentPage += 1;
if ([strongSelf.delegate respondsToSelector:@selector(personaSidebarView:requestPersonasAtPage:)]) {
[strongSelf.delegate personaSidebarView:strongSelf requestPersonasAtPage:strongSelf.currentPage];
}
}];
footer.stateLabel.hidden = YES;
footer.backgroundColor = [UIColor clearColor];
footer.automaticallyHidden = YES;
self.tableView.mj_footer = footer;
}
#pragma mark - Public
- (void)requestPersonasIfNeeded {
if (self.personas.count == 0) {
self.currentPage = 1;
self.hasMore = YES;
if ([self.delegate respondsToSelector:@selector(personaSidebarView:requestPersonasAtPage:)]) {
[self.delegate personaSidebarView:self requestPersonasAtPage:self.currentPage];
}
return;
}
[self applyFilterAndReload];
}
- (void)updatePersonas:(NSArray<KBPersonaModel *> *)personas
reset:(BOOL)reset
hasMore:(BOOL)hasMore
currentPage:(NSInteger)currentPage {
self.hasMore = hasMore;
NSInteger safePage = MAX(1, currentPage);
// HomeVC
self.personas = personas ?: @[];
self.currentPage = safePage;
[self applyFilterAndReload];
[self endLoadingMore];
}
- (void)updateSelectedPersonaId:(NSInteger)personaId {
self.selectedPersonaId = personaId;
[self.tableView reloadData];
}
- (void)endLoadingMore {
if ([self.tableView.mj_footer isRefreshing]) {
if (self.hasMore) {
[self.tableView.mj_footer endRefreshing];
} else {
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
}
}
- (void)resetLoadMore {
[self.tableView.mj_footer resetNoMoreData];
}
#pragma mark - Search
- (void)searchFieldChanged:(UITextField *)textField {
[self applyFilterAndReload];
}
- (void)applyFilterAndReload {
NSString *keyword = [self.searchField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (keyword.length == 0) {
self.displayPersonas = self.personas;
} else {
NSMutableArray *result = [NSMutableArray array];
for (KBPersonaModel *persona in self.personas) {
NSString *name = persona.name ?: @"";
NSString *desc = persona.shortDesc ?: persona.introText ?: @"";
if ([name localizedCaseInsensitiveContainsString:keyword] ||
[desc localizedCaseInsensitiveContainsString:keyword]) {
[result addObject:persona];
}
}
self.displayPersonas = result;
}
[self.tableView reloadData];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.displayPersonas.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAIPersonaSidebarCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KBAIPersonaSidebarCell"
forIndexPath:indexPath];
KBPersonaModel *persona = self.displayPersonas[indexPath.row];
BOOL selected = (persona.personaId == self.selectedPersonaId);
[cell configureWithPersona:persona selected:selected];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 72.0;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= self.displayPersonas.count) {
return;
}
KBPersonaModel *persona = self.displayPersonas[indexPath.row];
self.selectedPersonaId = persona.personaId;
[self.tableView reloadData];
if ([self.delegate respondsToSelector:@selector(personaSidebarView:didSelectPersona:)]) {
[self.delegate personaSidebarView:self didSelectPersona:persona];
}
}
#pragma mark - Lazy
- (UIVisualEffectView *)blurView {
if (!_blurView) {
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
_blurView = [[UIVisualEffectView alloc] initWithEffect:effect];
}
return _blurView;
}
- (UIView *)contentView {
if (!_contentView) {
_contentView = [[UIView alloc] init];
_contentView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.25];
}
return _contentView;
}
- (UIView *)searchContainer {
if (!_searchContainer) {
_searchContainer = [[UIView alloc] init];
_searchContainer.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
_searchContainer.layer.cornerRadius = 18;
_searchContainer.clipsToBounds = YES;
}
return _searchContainer;
}
- (UIImageView *)searchIconView {
if (!_searchIconView) {
_searchIconView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"magnifyingglass"]];
_searchIconView.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
}
return _searchIconView;
}
- (UITextField *)searchField {
if (!_searchField) {
_searchField = [[UITextField alloc] init];
_searchField.placeholder = KBLocalized(@"Search Role");
_searchField.textColor = [UIColor whiteColor];
_searchField.font = [UIFont systemFontOfSize:14];
_searchField.clearButtonMode = UITextFieldViewModeWhileEditing;
[_searchField addTarget:self action:@selector(searchFieldChanged:) forControlEvents:UIControlEventEditingChanged];
}
return _searchField;
}
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor clearColor];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.showsVerticalScrollIndicator = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
[_tableView registerClass:[KBAIPersonaSidebarCell class] forCellReuseIdentifier:@"KBAIPersonaSidebarCell"];
}
return _tableView;
}
@end

View File

@@ -19,10 +19,11 @@
#import "LSTPopView.h" #import "LSTPopView.h"
#import "KBAIMessageVC.h" #import "KBAIMessageVC.h"
#import "KBAICommentInputView.h" #import "KBAICommentInputView.h"
#import "KBAIPersonaSidebarView.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> @interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate, KBAIPersonaSidebarViewDelegate>
/// ///
@property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UICollectionView *collectionView;
@@ -90,11 +91,22 @@
/// ///
@property (nonatomic, strong) UIButton *messageButton; @property (nonatomic, strong) UIButton *messageButton;
///
@property (nonatomic, strong) UIButton *sidebarButton;
/// PopView
@property (nonatomic, weak) LSTPopView *sidebarPopView;
@property (nonatomic, strong) KBAIPersonaSidebarView *sidebarView;
/// ID
@property (nonatomic, assign) NSInteger selectedPersonaId;
@end @end
@implementation KBAIHomeVC @implementation KBAIHomeVC
static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
#pragma mark - Keyboard Gate #pragma mark - Keyboard Gate
/// view firstResponder /// view firstResponder
@@ -195,6 +207,14 @@
make.width.height.mas_equalTo(32); make.width.height.mas_equalTo(32);
}]; }];
//
[self.view addSubview:self.sidebarButton];
[self.sidebarButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(KB_STATUSBAR_HEIGHT + 10);
make.left.equalTo(self.view).offset(16);
make.width.height.mas_equalTo(32);
}];
// //
[self.view addSubview:self.bottomBackgroundView]; [self.view addSubview:self.bottomBackgroundView];
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView]; [self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
@@ -262,6 +282,7 @@
} }
self.isLoading = YES; self.isLoading = YES;
NSInteger oldCount = self.personas.count;
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self;
[self.aiVM fetchPersonasWithPageNum:self.currentPage [self.aiVM fetchPersonasWithPageNum:self.currentPage
@@ -283,9 +304,31 @@
weakSelf.hasMore = pageModel.hasMore; weakSelf.hasMore = pageModel.hasMore;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.collectionView reloadData];
if (weakSelf.currentPage == 1) { if (weakSelf.currentPage == 1) {
[weakSelf.collectionView reloadData];
[weakSelf preloadDataForIndexes:@[@0, @1, @2]]; [weakSelf preloadDataForIndexes:@[@0, @1, @2]];
} else if (pageModel.records.count > 0) {
NSInteger newCount = weakSelf.personas.count;
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
for (NSInteger i = oldCount; i < newCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:0]];
}
[UIView performWithoutAnimation:^{
[weakSelf.collectionView performBatchUpdates:^{
[weakSelf.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:nil];
}];
}
if (weakSelf.selectedPersonaId <= 0 && weakSelf.personas.count > 0) {
NSInteger index = MIN(MAX(weakSelf.currentIndex, 0), weakSelf.personas.count - 1);
[weakSelf storeSelectedPersonaId:weakSelf.personas[index].personaId];
}
if (weakSelf.sidebarView) {
[weakSelf.sidebarView updatePersonas:weakSelf.personas
reset:(weakSelf.currentPage == 1)
hasMore:weakSelf.hasMore
currentPage:weakSelf.currentPage];
[weakSelf.sidebarView updateSelectedPersonaId:[weakSelf storedSelectedPersonaId]];
} }
}); });
@@ -644,8 +687,44 @@
return nil; return nil;
} }
- (NSInteger)indexOfPersonaId:(NSInteger)personaId {
if (personaId <= 0) {
return NSNotFound;
}
for (NSInteger i = 0; i < self.personas.count; i++) {
KBPersonaModel *persona = self.personas[i];
if (persona.personaId == personaId) {
return i;
}
}
return NSNotFound;
}
#pragma mark - Private #pragma mark - Private
- (NSInteger)storedSelectedPersonaId {
NSInteger savedId = [[NSUserDefaults standardUserDefaults] integerForKey:KBAISelectedPersonaIdKey];
if (savedId > 0) {
return savedId;
}
if (self.currentIndex >= 0 && self.currentIndex < self.personas.count) {
return self.personas[self.currentIndex].personaId;
}
return 0;
}
- (void)storeSelectedPersonaId:(NSInteger)personaId {
if (personaId <= 0) {
return;
}
self.selectedPersonaId = personaId;
[[NSUserDefaults standardUserDefaults] setInteger:personaId forKey:KBAISelectedPersonaIdKey];
[[NSUserDefaults standardUserDefaults] synchronize];
if (self.sidebarView) {
[self.sidebarView updateSelectedPersonaId:personaId];
}
}
- (void)updateCollectionViewScrollState { - (void)updateCollectionViewScrollState {
BOOL shouldEnable = !self.isWaitingForAIResponse BOOL shouldEnable = !self.isWaitingForAIResponse
&& !self.isVoiceRecording && !self.isVoiceRecording
@@ -766,6 +845,46 @@
[KB_CURRENT_NAV pushViewController:vc animated:true]; [KB_CURRENT_NAV pushViewController:vc animated:true];
} }
#pragma mark - KBAIPersonaSidebarViewDelegate
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
requestPersonasAtPage:(NSInteger)page {
if (self.isLoading) {
[view endLoadingMore];
return;
}
self.currentPage = MAX(1, page);
if (self.currentPage == 1) {
[self.personas removeAllObjects];
[view resetLoadMore];
}
[self loadPersonas];
}
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
didSelectPersona:(KBPersonaModel *)persona {
if (!persona) {
return;
}
[self storeSelectedPersonaId:persona.personaId];
NSInteger index = [self indexOfPersonaId:persona.personaId];
if (index != NSNotFound) {
self.currentIndex = index;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
[self preloadAdjacentCellsForIndex:index];
[self saveSelectedPersonaToAppGroup:persona];
}
if (self.sidebarPopView) {
[self.sidebarPopView dismiss];
}
}
- (UIView *)bottomBackgroundView { - (UIView *)bottomBackgroundView {
if (!_bottomBackgroundView) { if (!_bottomBackgroundView) {
_bottomBackgroundView = [[UIView alloc] init]; _bottomBackgroundView = [[UIView alloc] init];
@@ -807,6 +926,16 @@
return _messageButton; return _messageButton;
} }
- (UIButton *)sidebarButton {
if (!_sidebarButton) {
_sidebarButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *icon = [UIImage imageNamed:@"ai_more_icon"];
[_sidebarButton setImage:icon forState:UIControlStateNormal];
[_sidebarButton addTarget:self action:@selector(sidebarButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _sidebarButton;
}
#pragma mark - Actions #pragma mark - Actions
- (void)messageButtonTapped { - (void)messageButtonTapped {
@@ -814,6 +943,41 @@
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
} }
- (void)sidebarButtonTapped {
[self showPersonaSidebar];
}
- (void)showPersonaSidebar {
if (!self.sidebarView) {
CGFloat width = KB_SCREEN_WIDTH * 0.7;
CGFloat height = KB_SCREEN_HEIGHT;
self.sidebarView = [[KBAIPersonaSidebarView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
self.sidebarView.delegate = self;
}
self.sidebarView.selectedPersonaId = [self storedSelectedPersonaId];
[self.sidebarView updatePersonas:self.personas
reset:YES
hasMore:self.hasMore
currentPage:self.currentPage];
[self.sidebarView requestPersonasIfNeeded];
if (self.sidebarPopView) {
[self.sidebarPopView dismiss];
}
LSTPopView *popView = [LSTPopView initWithCustomView:self.sidebarView
parentView:nil
popStyle:LSTPopStyleSmoothFromLeft
dismissStyle:LSTDismissStyleSmoothToLeft];
popView.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.35];
popView.hemStyle = LSTHemStyleLeft;
popView.isClickBgDismiss = YES;
popView.isAvoidKeyboard = NO;
self.sidebarPopView = popView;
[popView pop];
}
/// - handleTranscribedText /// - handleTranscribedText
- (void)handleCommentInputSend:(NSString *)text { - (void)handleCommentInputSend:(NSString *)text {
NSString *trimmedText = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *trimmedText = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];