diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 6e1768c..f2745d5 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ 04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; }; 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.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 */; }; 04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039432F236E75002CA5A0 /* KBChatAssistantMessageCell.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 = ""; }; 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatLimitPopView.h; sourceTree = ""; }; 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatLimitPopView.m; sourceTree = ""; }; + 0F2A10112F3C0002002CA5A0 /* KBAIPersonaSidebarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIPersonaSidebarView.h; sourceTree = ""; }; + 0F2A10122F3C0002002CA5A0 /* KBAIPersonaSidebarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIPersonaSidebarView.m; sourceTree = ""; }; 048FFD252F28C6CF005D62AE /* KBImagePositionButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBImagePositionButton.h; sourceTree = ""; }; 048FFD262F28C6CF005D62AE /* KBImagePositionButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBImagePositionButton.m; sourceTree = ""; }; 048FFD282F28E99A005D62AE /* KBCommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCommentModel.h; sourceTree = ""; }; @@ -1449,6 +1452,8 @@ children = ( 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */, 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */, + 0F2A10112F3C0002002CA5A0 /* KBAIPersonaSidebarView.h */, + 0F2A10122F3C0002002CA5A0 /* KBAIPersonaSidebarView.m */, ); path = PopView; sourceTree = ""; @@ -2419,6 +2424,7 @@ 046086D92F1A093400757C95 /* KBAICommentHeaderView.m in Sources */, 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */, 0F2A10032F3C0001002CA5A0 /* KBChatMessageActionPopView.m in Sources */, + 0F2A10132F3C0002002CA5A0 /* KBAIPersonaSidebarView.m in Sources */, 04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */, 04E0394D2F236E75002CA5A0 /* KBChatAssistantMessageCell.m in Sources */, 04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */, diff --git a/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/Contents.json new file mode 100644 index 0000000..2297ee2 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/Contents.json @@ -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 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@2x.png b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@2x.png new file mode 100644 index 0000000..7418f5f Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@2x.png differ diff --git a/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@3x.png b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@3x.png new file mode 100644 index 0000000..c0ee5c6 Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@3x.png differ diff --git a/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/Contents.json new file mode 100644 index 0000000..85205cb --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/Contents.json @@ -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 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@2x.png b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@2x.png new file mode 100644 index 0000000..c31306e Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@2x.png differ diff --git a/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@3x.png b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@3x.png new file mode 100644 index 0000000..493e78d Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@3x.png differ diff --git a/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/Contents.json new file mode 100644 index 0000000..2dc1228 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/Contents.json @@ -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 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@2x.png b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@2x.png new file mode 100644 index 0000000..a4d6b3c Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@2x.png differ diff --git a/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@3x.png b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@3x.png new file mode 100644 index 0000000..4f69d32 Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@3x.png differ diff --git a/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/Contents.json new file mode 100644 index 0000000..53f4b80 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/Contents.json @@ -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 + } +} diff --git a/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@2x.png b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@2x.png new file mode 100644 index 0000000..14f4671 Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@2x.png differ diff --git a/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@3x.png b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@3x.png new file mode 100644 index 0000000..8aef327 Binary files /dev/null and b/keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@3x.png differ diff --git a/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h b/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h new file mode 100644 index 0000000..05c3ff3 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h @@ -0,0 +1,51 @@ +// +// KBAIPersonaSidebarView.h +// keyBoard +// +// Created by Codex on 2026/2/3. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class KBPersonaModel; +@class KBAIPersonaSidebarView; + +@protocol KBAIPersonaSidebarViewDelegate +@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 delegate; +@property (nonatomic, assign) NSInteger selectedPersonaId; +@property (nonatomic, assign, readonly) NSInteger currentPage; + +/// 更新人设列表 +/// reset=YES 表示重置并替换数据;reset=NO 表示追加 +/// currentPage 为当前已加载页数(从 1 开始) +- (void)updatePersonas:(NSArray *)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 diff --git a/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m b/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m new file mode 100644 index 0000000..5e29ae9 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m @@ -0,0 +1,417 @@ +// +// KBAIPersonaSidebarView.m +// keyBoard +// +// Created by Codex on 2026/2/3. +// + +#import "KBAIPersonaSidebarView.h" +#import "KBPersonaModel.h" +#import +#import +#import + +#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 () + +@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 *personas; +@property (nonatomic, strong) NSArray *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 *)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 diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index 0b3ca82..bf44884 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -19,10 +19,11 @@ #import "LSTPopView.h" #import "KBAIMessageVC.h" #import "KBAICommentInputView.h" +#import "KBAIPersonaSidebarView.h" #import #import -@interface KBAIHomeVC () +@interface KBAIHomeVC () /// 人设列表容器 @property (nonatomic, strong) UICollectionView *collectionView; @@ -90,11 +91,22 @@ /// 右上角消息按钮 @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 @implementation KBAIHomeVC +static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId"; + #pragma mark - Keyboard Gate /// 查找当前 view 树里的 firstResponder @@ -194,6 +206,14 @@ make.right.equalTo(self.view).offset(-16); 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]; @@ -262,6 +282,7 @@ } self.isLoading = YES; + NSInteger oldCount = self.personas.count; __weak typeof(self) weakSelf = self; [self.aiVM fetchPersonasWithPageNum:self.currentPage @@ -283,9 +304,31 @@ weakSelf.hasMore = pageModel.hasMore; dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf.collectionView reloadData]; if (weakSelf.currentPage == 1) { + [weakSelf.collectionView reloadData]; [weakSelf preloadDataForIndexes:@[@0, @1, @2]]; + } else if (pageModel.records.count > 0) { + NSInteger newCount = weakSelf.personas.count; + NSMutableArray *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; } +- (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 +- (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 { BOOL shouldEnable = !self.isWaitingForAIResponse && !self.isVoiceRecording @@ -766,6 +845,46 @@ [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 { if (!_bottomBackgroundView) { _bottomBackgroundView = [[UIView alloc] init]; @@ -807,6 +926,16 @@ 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 - (void)messageButtonTapped { @@ -814,6 +943,41 @@ [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 - (void)handleCommentInputSend:(NSString *)text { NSString *trimmedText = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];