// // 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