// // 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.mas_equalTo(6); make.height.mas_equalTo(8); }]; [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) BaseTableView *tableView; @property (nonatomic, strong) UIView *searchResultContainer; @property (nonatomic, strong) BaseTableView *searchResultTableView; @property (nonatomic, strong) NSArray *personas; @property (nonatomic, strong) NSArray *displayPersonas; @property (nonatomic, strong) NSArray *searchResults; @property (nonatomic, assign) BOOL isShowingSearchResults; @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.contentView addSubview:self.searchResultContainer]; [self.searchResultContainer addSubview:self.searchResultTableView]; [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(KB_STATUSBAR_HEIGHT + 20); 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); }]; [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); }]; __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; NSLog(@"[SidebarSearch] 请求人设数据: page=%ld", (long)self.currentPage); 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; NSLog(@"[SidebarSearch] 更新人设: count=%ld, page=%ld, hasMore=%@", (long)self.personas.count, (long)self.currentPage, self.hasMore ? @"YES" : @"NO"); [self applyFilterAndReload]; if (self.isShowingSearchResults && self.searchField.text.length > 0) { [self performSearch]; } [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 { NSString *keyword = [textField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSLog(@"[SidebarSearch] 输入变化: \"%@\"", keyword); if (keyword.length == 0) { [self hideSearchResults]; } } - (void)applyFilterAndReload { self.displayPersonas = self.personas; [self.tableView reloadData]; } - (void)performSearch { NSString *keyword = [self.searchField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSLog(@"[SidebarSearch] 执行搜索: \"%@\" (total=%ld)", keyword, (long)self.personas.count); if (keyword.length == 0) { [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]; } } 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] 隐藏搜索结果视图"); } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (tableView == self.searchResultTableView) { return self.searchResults.count; } return self.displayPersonas.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { KBAIPersonaSidebarCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KBAIPersonaSidebarCell" forIndexPath:indexPath]; KBPersonaModel *persona = (tableView == self.searchResultTableView) ? self.searchResults[indexPath.row] : 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 { NSArray *source = (tableView == self.searchResultTableView) ? self.searchResults : self.displayPersonas; if (indexPath.row >= source.count) { return; } KBPersonaModel *persona = source[indexPath.row]; self.selectedPersonaId = persona.personaId; [self.tableView reloadData]; if (self.isShowingSearchResults) { [self.searchResultTableView 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.returnKeyType = UIReturnKeySearch; _searchField.delegate = self; [_searchField addTarget:self action:@selector(searchFieldChanged:) forControlEvents:UIControlEventEditingChanged]; [_searchField addTarget:self action:@selector(searchFieldReturnTapped:) forControlEvents:UIControlEventEditingDidEndOnExit]; } return _searchField; } - (BaseTableView *)tableView { if (!_tableView) { _tableView = [[BaseTableView 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; } - (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]; } } @end