Files
keyboard/keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m

530 lines
19 KiB
Mathematica
Raw Normal View History

2026-02-03 16:54:38 +08:00
//
// 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);
2026-02-03 18:03:21 +08:00
make.width.mas_equalTo(6);
make.height.mas_equalTo(8);
2026-02-03 16:54:38 +08:00
}];
[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;
2026-02-03 17:00:42 +08:00
@property (nonatomic, strong) BaseTableView *tableView;
2026-02-03 18:03:21 +08:00
@property (nonatomic, strong) UIView *searchResultContainer;
@property (nonatomic, strong) BaseTableView *searchResultTableView;
2026-02-03 16:54:38 +08:00
@property (nonatomic, strong) NSArray<KBPersonaModel *> *personas;
@property (nonatomic, strong) NSArray<KBPersonaModel *> *displayPersonas;
2026-02-03 18:03:21 +08:00
@property (nonatomic, strong) NSArray<KBPersonaModel *> *searchResults;
@property (nonatomic, assign) BOOL isShowingSearchResults;
2026-02-03 16:54:38 +08:00
@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];
2026-02-03 18:03:21 +08:00
[self.contentView addSubview:self.searchResultContainer];
[self.searchResultContainer addSubview:self.searchResultTableView];
2026-02-03 16:54:38 +08:00
[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) {
2026-02-03 17:00:42 +08:00
make.top.equalTo(self.contentView).offset(KB_STATUSBAR_HEIGHT + 20);
2026-02-03 16:54:38 +08:00
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);
}];
2026-02-03 18:03:21 +08:00
[self.searchResultContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.searchContainer.mas_bottom).offset(10);
make.left.right.bottom.equalTo(self.contentView);
}];
[self.searchResultTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.searchResultContainer);
}];
2026-02-03 16:54:38 +08:00
__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;
2026-02-03 18:03:21 +08:00
NSLog(@"[SidebarSearch] 请求人设数据: page=%ld", (long)self.currentPage);
2026-02-03 16:54:38 +08:00
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;
2026-02-03 18:03:21 +08:00
NSLog(@"[SidebarSearch] 更新人设: count=%ld, page=%ld, hasMore=%@",
(long)self.personas.count, (long)self.currentPage, self.hasMore ? @"YES" : @"NO");
2026-02-03 16:54:38 +08:00
[self applyFilterAndReload];
2026-02-03 18:03:21 +08:00
if (self.isShowingSearchResults && self.searchField.text.length > 0) {
[self performSearch];
}
2026-02-03 16:54:38 +08:00
[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 {
2026-02-03 18:03:21 +08:00
NSString *keyword = [textField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSLog(@"[SidebarSearch] 输入变化: \"%@\"", keyword);
if (keyword.length == 0) {
[self hideSearchResults];
}
2026-02-03 16:54:38 +08:00
}
- (void)applyFilterAndReload {
2026-02-03 18:03:21 +08:00
self.displayPersonas = self.personas;
[self.tableView reloadData];
}
- (void)performSearch {
2026-02-03 16:54:38 +08:00
NSString *keyword = [self.searchField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
2026-02-03 18:03:21 +08:00
NSLog(@"[SidebarSearch] 执行搜索: \"%@\" (total=%ld)", keyword, (long)self.personas.count);
2026-02-03 16:54:38 +08:00
if (keyword.length == 0) {
2026-02-03 18:03:21 +08:00
[self hideSearchResults];
return;
}
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];
2026-02-03 16:54:38 +08:00
}
}
2026-02-03 18:03:21 +08:00
self.searchResults = result;
NSLog(@"[SidebarSearch] 搜索结果: %ld 条", (long)self.searchResults.count);
if (self.searchResults.count > 0) {
[self showSearchResults];
[self.searchResultTableView reloadData];
} else {
[self hideSearchResults];
}
}
- (void)showSearchResults {
self.isShowingSearchResults = YES;
self.tableView.hidden = YES;
self.searchResultContainer.hidden = NO;
[self.contentView bringSubviewToFront:self.searchResultContainer];
NSLog(@"[SidebarSearch] 显示搜索结果视图");
}
- (void)hideSearchResults {
self.isShowingSearchResults = NO;
self.searchResults = @[];
self.searchResultContainer.hidden = YES;
self.tableView.hidden = NO;
NSLog(@"[SidebarSearch] 隐藏搜索结果视图");
2026-02-03 16:54:38 +08:00
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
2026-02-03 18:03:21 +08:00
if (tableView == self.searchResultTableView) {
return self.searchResults.count;
}
2026-02-03 16:54:38 +08:00
return self.displayPersonas.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAIPersonaSidebarCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KBAIPersonaSidebarCell"
forIndexPath:indexPath];
2026-02-03 18:03:21 +08:00
KBPersonaModel *persona = (tableView == self.searchResultTableView)
? self.searchResults[indexPath.row]
: self.displayPersonas[indexPath.row];
2026-02-03 16:54:38 +08:00
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 {
2026-02-03 18:03:21 +08:00
NSArray *source = (tableView == self.searchResultTableView) ? self.searchResults : self.displayPersonas;
if (indexPath.row >= source.count) {
2026-02-03 16:54:38 +08:00
return;
}
2026-02-03 18:03:21 +08:00
KBPersonaModel *persona = source[indexPath.row];
2026-02-03 16:54:38 +08:00
self.selectedPersonaId = persona.personaId;
[self.tableView reloadData];
2026-02-03 18:03:21 +08:00
if (self.isShowingSearchResults) {
[self.searchResultTableView reloadData];
}
2026-02-03 16:54:38 +08:00
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");
2026-02-03 20:51:48 +08:00
_searchField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:KBLocalized(@"Search Role")
attributes:@{NSForegroundColorAttributeName: [UIColor whiteColor]}];
2026-02-03 16:54:38 +08:00
_searchField.textColor = [UIColor whiteColor];
_searchField.font = [UIFont systemFontOfSize:14];
_searchField.clearButtonMode = UITextFieldViewModeWhileEditing;
2026-02-03 17:00:42 +08:00
_searchField.returnKeyType = UIReturnKeySearch;
2026-02-03 18:03:21 +08:00
_searchField.delegate = self;
2026-02-03 16:54:38 +08:00
[_searchField addTarget:self action:@selector(searchFieldChanged:) forControlEvents:UIControlEventEditingChanged];
2026-02-03 18:03:21 +08:00
[_searchField addTarget:self action:@selector(searchFieldReturnTapped:) forControlEvents:UIControlEventEditingDidEndOnExit];
2026-02-03 16:54:38 +08:00
}
return _searchField;
}
2026-02-03 17:00:42 +08:00
- (BaseTableView *)tableView {
2026-02-03 16:54:38 +08:00
if (!_tableView) {
2026-02-03 17:00:42 +08:00
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
2026-02-03 16:54:38 +08:00
_tableView.backgroundColor = [UIColor clearColor];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.showsVerticalScrollIndicator = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
[_tableView registerClass:[KBAIPersonaSidebarCell class] forCellReuseIdentifier:@"KBAIPersonaSidebarCell"];
}
return _tableView;
}
2026-02-03 18:03:21 +08:00
- (UIView *)searchResultContainer {
if (!_searchResultContainer) {
_searchResultContainer = [[UIView alloc] init];
_searchResultContainer.backgroundColor = [UIColor clearColor];
_searchResultContainer.hidden = YES;
}
return _searchResultContainer;
}
- (BaseTableView *)searchResultTableView {
if (!_searchResultTableView) {
_searchResultTableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_searchResultTableView.backgroundColor = [UIColor clearColor];
_searchResultTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_searchResultTableView.showsVerticalScrollIndicator = NO;
_searchResultTableView.delegate = self;
_searchResultTableView.dataSource = self;
[_searchResultTableView registerClass:[KBAIPersonaSidebarCell class] forCellReuseIdentifier:@"KBAIPersonaSidebarCell"];
}
return _searchResultTableView;
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (textField == self.searchField) {
NSLog(@"[SidebarSearch] textFieldShouldReturn");
[textField resignFirstResponder];
[self performSearch];
return YES;
}
return NO;
}
- (void)searchFieldReturnTapped:(UITextField *)textField {
if (textField == self.searchField) {
NSLog(@"[SidebarSearch] EditingDidEndOnExit");
[textField resignFirstResponder];
[self performSearch];
}
}
2026-02-03 16:54:38 +08:00
@end