Compare commits
7 Commits
dev_语音_tab
...
822a814f85
| Author | SHA1 | Date | |
|---|---|---|---|
| 822a814f85 | |||
| 0bd0392191 | |||
| b9663037f5 | |||
| a0923c8572 | |||
| d482cfcb7d | |||
| 9e6d2906f8 | |||
| 6f7bb4f960 |
@@ -78,6 +78,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||||
@property(nonatomic, strong) UIImageView *personaAvatarImageView; // 语音模式下显示的 persona 小头像
|
@property(nonatomic, strong) UIImageView *personaAvatarImageView; // 语音模式下显示的 persona 小头像
|
||||||
@property(nonatomic, strong) UIImageView *personaGrayImageView; // 语音模式下显示的 persona 小头像
|
@property(nonatomic, strong) UIImageView *personaGrayImageView; // 语音模式下显示的 persona 小头像
|
||||||
|
@property(nonatomic, strong) UIVisualEffectView *personaBlurView; // 语音模式下头像高斯模糊层
|
||||||
|
|
||||||
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
||||||
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
@@ -1407,6 +1408,10 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
_personaAvatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
_personaAvatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||||
_personaAvatarImageView.clipsToBounds = YES;
|
_personaAvatarImageView.clipsToBounds = YES;
|
||||||
_personaAvatarImageView.hidden = YES;
|
_personaAvatarImageView.hidden = YES;
|
||||||
|
[_personaAvatarImageView addSubview:self.personaBlurView];
|
||||||
|
[self.personaBlurView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(_personaAvatarImageView);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
return _personaAvatarImageView;
|
return _personaAvatarImageView;
|
||||||
}
|
}
|
||||||
@@ -1419,6 +1424,16 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
return _personaGrayImageView;
|
return _personaGrayImageView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIVisualEffectView *)personaBlurView {
|
||||||
|
if (!_personaBlurView) {
|
||||||
|
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||||
|
_personaBlurView = [[UIVisualEffectView alloc] initWithEffect:effect];
|
||||||
|
_personaBlurView.hidden = YES;
|
||||||
|
_personaBlurView.userInteractionEnabled = NO;
|
||||||
|
}
|
||||||
|
return _personaBlurView;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Persona Avatar
|
#pragma mark - Persona Avatar
|
||||||
|
|
||||||
/// 从 AppGroup 读取选中的 persona 信息
|
/// 从 AppGroup 读取选中的 persona 信息
|
||||||
@@ -1475,6 +1490,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
if (image) {
|
if (image) {
|
||||||
self.personaAvatarImageView.image = image;
|
self.personaAvatarImageView.image = image;
|
||||||
self.personaAvatarImageView.hidden = NO;
|
self.personaAvatarImageView.hidden = NO;
|
||||||
|
self.personaBlurView.hidden = NO;
|
||||||
NSLog(@"[Keyboard] persona 封面图加载成功");
|
NSLog(@"[Keyboard] persona 封面图加载成功");
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"[Keyboard] persona 封面图加载失败");
|
NSLog(@"[Keyboard] persona 封面图加载失败");
|
||||||
@@ -1485,6 +1501,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
- (void)kb_hidePersonaAvatar {
|
- (void)kb_hidePersonaAvatar {
|
||||||
self.personaAvatarImageView.hidden = YES;
|
self.personaAvatarImageView.hidden = YES;
|
||||||
self.personaAvatarImageView.image = nil;
|
self.personaAvatarImageView.image = nil;
|
||||||
|
self.personaBlurView.hidden = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
|
|||||||
@@ -229,6 +229,8 @@
|
|||||||
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */; };
|
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */; };
|
||||||
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 */; };
|
||||||
|
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 */; };
|
||||||
@@ -562,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>"; };
|
||||||
@@ -720,6 +724,8 @@
|
|||||||
04E039482F236E75002CA5A0 /* KBChatTimeCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatTimeCell.m; sourceTree = "<group>"; };
|
04E039482F236E75002CA5A0 /* KBChatTimeCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatTimeCell.m; sourceTree = "<group>"; };
|
||||||
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatUserMessageCell.h; sourceTree = "<group>"; };
|
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatUserMessageCell.h; sourceTree = "<group>"; };
|
||||||
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatUserMessageCell.m; sourceTree = "<group>"; };
|
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatUserMessageCell.m; sourceTree = "<group>"; };
|
||||||
|
0F2A10012F3C0001002CA5A0 /* KBChatMessageActionPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatMessageActionPopView.h; sourceTree = "<group>"; };
|
||||||
|
0F2A10022F3C0001002CA5A0 /* KBChatMessageActionPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatMessageActionPopView.m; sourceTree = "<group>"; };
|
||||||
04E039502F2387D2002CA5A0 /* KBAiChatMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAiChatMessage.h; sourceTree = "<group>"; };
|
04E039502F2387D2002CA5A0 /* KBAiChatMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAiChatMessage.h; sourceTree = "<group>"; };
|
||||||
04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAiChatMessage.m; sourceTree = "<group>"; };
|
04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAiChatMessage.m; sourceTree = "<group>"; };
|
||||||
04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = "<group>"; };
|
04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = "<group>"; };
|
||||||
@@ -1431,6 +1437,8 @@
|
|||||||
04E039452F236E75002CA5A0 /* KBChatTableView.m */,
|
04E039452F236E75002CA5A0 /* KBChatTableView.m */,
|
||||||
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */,
|
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */,
|
||||||
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */,
|
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */,
|
||||||
|
0F2A10012F3C0001002CA5A0 /* KBChatMessageActionPopView.h */,
|
||||||
|
0F2A10022F3C0001002CA5A0 /* KBChatMessageActionPopView.m */,
|
||||||
04E039422F236E75002CA5A0 /* KBChatAssistantMessageCell.h */,
|
04E039422F236E75002CA5A0 /* KBChatAssistantMessageCell.h */,
|
||||||
04E039432F236E75002CA5A0 /* KBChatAssistantMessageCell.m */,
|
04E039432F236E75002CA5A0 /* KBChatAssistantMessageCell.m */,
|
||||||
04E039472F236E75002CA5A0 /* KBChatTimeCell.h */,
|
04E039472F236E75002CA5A0 /* KBChatTimeCell.h */,
|
||||||
@@ -1444,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>";
|
||||||
@@ -2413,6 +2423,8 @@
|
|||||||
046086D82F1A093400757C95 /* KBAIReplyCell.m in Sources */,
|
046086D82F1A093400757C95 /* KBAIReplyCell.m in Sources */,
|
||||||
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 */,
|
||||||
|
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 */,
|
||||||
|
|||||||
22
keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
keyBoard/Assets.xcassets/AI/ai_more_icon.imageset/ai_more_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
22
keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
keyBoard/Assets.xcassets/AI/ai_role_sel.imageset/ai_role_sel@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
22
keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 923 B |
BIN
keyBoard/Assets.xcassets/AI/ai_search_icon.imageset/ai_search_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
22
keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 474 B |
BIN
keyBoard/Assets.xcassets/AI/right_arrow_icon.imageset/right_arrow_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 858 B |
33
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// KBChatMessageActionPopView.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBChatMessageActionType) {
|
||||||
|
KBChatMessageActionTypeCopy = 0,
|
||||||
|
KBChatMessageActionTypeDelete = 1,
|
||||||
|
KBChatMessageActionTypeReport = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
@class KBChatMessageActionPopView;
|
||||||
|
|
||||||
|
@protocol KBChatMessageActionPopViewDelegate <NSObject>
|
||||||
|
@optional
|
||||||
|
- (void)chatMessageActionPopView:(KBChatMessageActionPopView *)view
|
||||||
|
didSelectAction:(KBChatMessageActionType)action;
|
||||||
|
@end
|
||||||
|
|
||||||
|
/// 聊天消息长按操作弹窗(Copy / Delete / Report)
|
||||||
|
@interface KBChatMessageActionPopView : UIView
|
||||||
|
|
||||||
|
@property (nonatomic, weak) id<KBChatMessageActionPopViewDelegate> delegate;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
164
keyBoard/Class/AiTalk/V/Chat/KBChatMessageActionPopView.m
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
//
|
||||||
|
// KBChatMessageActionPopView.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBChatMessageActionPopView.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
static CGFloat const kKBChatActionRowHeight = 52.0;
|
||||||
|
|
||||||
|
@interface KBChatMessageActionPopView ()
|
||||||
|
|
||||||
|
@property (nonatomic, strong) UIControl *copyRow;
|
||||||
|
@property (nonatomic, strong) UIControl *deleteRow;
|
||||||
|
@property (nonatomic, strong) UIControl *reportRow;
|
||||||
|
@property (nonatomic, strong) UIView *line1;
|
||||||
|
@property (nonatomic, strong) UIView *line2;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBChatMessageActionPopView
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
|
self = [super initWithFrame:frame];
|
||||||
|
if (self) {
|
||||||
|
[self setupUI];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UI
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
self.backgroundColor = [UIColor colorWithWhite:0.1 alpha:0.92];
|
||||||
|
self.layer.cornerRadius = 16.0;
|
||||||
|
self.layer.masksToBounds = YES;
|
||||||
|
|
||||||
|
[self addSubview:self.copyRow];
|
||||||
|
[self addSubview:self.line1];
|
||||||
|
[self addSubview:self.deleteRow];
|
||||||
|
[self addSubview:self.line2];
|
||||||
|
[self addSubview:self.reportRow];
|
||||||
|
|
||||||
|
[self.copyRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.left.right.equalTo(self);
|
||||||
|
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.line1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.copyRow.mas_bottom);
|
||||||
|
make.left.right.equalTo(self);
|
||||||
|
make.height.mas_equalTo(0.5);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.deleteRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.line1.mas_bottom);
|
||||||
|
make.left.right.equalTo(self);
|
||||||
|
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.line2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.deleteRow.mas_bottom);
|
||||||
|
make.left.right.equalTo(self);
|
||||||
|
make.height.mas_equalTo(0.5);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.reportRow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.line2.mas_bottom);
|
||||||
|
make.left.right.bottom.equalTo(self);
|
||||||
|
make.height.mas_equalTo(kKBChatActionRowHeight);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIControl *)buildRowWithTitle:(NSString *)title
|
||||||
|
iconName:(NSString *)iconName
|
||||||
|
action:(KBChatMessageActionType)action {
|
||||||
|
UIControl *row = [[UIControl alloc] init];
|
||||||
|
row.tag = action;
|
||||||
|
[row addTarget:self action:@selector(actionRowTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
|
||||||
|
UILabel *label = [[UILabel alloc] init];
|
||||||
|
label.text = title;
|
||||||
|
label.textColor = [UIColor whiteColor];
|
||||||
|
label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightRegular];
|
||||||
|
|
||||||
|
UIImageView *iconView = [[UIImageView alloc] init];
|
||||||
|
UIImage *icon = [UIImage systemImageNamed:iconName];
|
||||||
|
iconView.image = icon;
|
||||||
|
iconView.tintColor = [UIColor whiteColor];
|
||||||
|
iconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
|
|
||||||
|
[row addSubview:label];
|
||||||
|
[row addSubview:iconView];
|
||||||
|
|
||||||
|
[label mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(row).offset(16);
|
||||||
|
make.centerY.equalTo(row);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.right.equalTo(row).offset(-16);
|
||||||
|
make.centerY.equalTo(row);
|
||||||
|
make.width.height.mas_equalTo(18);
|
||||||
|
}];
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
- (void)actionRowTapped:(UIControl *)sender {
|
||||||
|
if ([self.delegate respondsToSelector:@selector(chatMessageActionPopView:didSelectAction:)]) {
|
||||||
|
[self.delegate chatMessageActionPopView:self didSelectAction:(KBChatMessageActionType)sender.tag];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UIControl *)copyRow {
|
||||||
|
if (!_copyRow) {
|
||||||
|
_copyRow = [self buildRowWithTitle:KBLocalized(@"Copy")
|
||||||
|
iconName:@"doc.on.doc"
|
||||||
|
action:KBChatMessageActionTypeCopy];
|
||||||
|
}
|
||||||
|
return _copyRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIControl *)deleteRow {
|
||||||
|
if (!_deleteRow) {
|
||||||
|
_deleteRow = [self buildRowWithTitle:KBLocalized(@"Delete")
|
||||||
|
iconName:@"trash"
|
||||||
|
action:KBChatMessageActionTypeDelete];
|
||||||
|
}
|
||||||
|
return _deleteRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIControl *)reportRow {
|
||||||
|
if (!_reportRow) {
|
||||||
|
_reportRow = [self buildRowWithTitle:KBLocalized(@"Report")
|
||||||
|
iconName:@"exclamationmark.circle"
|
||||||
|
action:KBChatMessageActionTypeReport];
|
||||||
|
}
|
||||||
|
return _reportRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIView *)line1 {
|
||||||
|
if (!_line1) {
|
||||||
|
_line1 = [[UIView alloc] init];
|
||||||
|
_line1.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||||
|
}
|
||||||
|
return _line1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIView *)line2 {
|
||||||
|
if (!_line2) {
|
||||||
|
_line2 = [[UIView alloc] init];
|
||||||
|
_line2.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||||
|
}
|
||||||
|
return _line2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -17,6 +17,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
|
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
|
||||||
scrollView:(UIScrollView *)scrollView;
|
scrollView:(UIScrollView *)scrollView;
|
||||||
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView;
|
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView;
|
||||||
|
/// 长按消息(用户/AI)
|
||||||
|
- (void)chatTableView:(KBChatTableView *)chatView
|
||||||
|
didLongPressMessage:(KBAiChatMessage *)message
|
||||||
|
sourceRect:(CGRect)sourceRect;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/// 聊天列表视图(支持用户消息、AI 消息、时间戳、语音播放)
|
/// 聊天列表视图(支持用户消息、AI 消息、时间戳、语音播放)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
@property (nonatomic, strong) UILabel *topStatusLabel;
|
@property (nonatomic, strong) UILabel *topStatusLabel;
|
||||||
@property (nonatomic, assign) BOOL isTopLoading;
|
@property (nonatomic, assign) BOOL isTopLoading;
|
||||||
@property (nonatomic, assign) BOOL isTopNoMore;
|
@property (nonatomic, assign) BOOL isTopNoMore;
|
||||||
|
@property (nonatomic, strong) UILongPressGestureRecognizer *messageLongPressGesture;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -101,6 +102,13 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
make.edges.equalTo(self);
|
make.edges.equalTo(self);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// 长按消息操作
|
||||||
|
self.messageLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||||
|
action:@selector(handleMessageLongPress:)];
|
||||||
|
self.messageLongPressGesture.minimumPressDuration = 0.4;
|
||||||
|
self.messageLongPressGesture.cancelsTouchesInView = YES;
|
||||||
|
[self.tableView addGestureRecognizer:self.messageLongPressGesture];
|
||||||
|
|
||||||
// 初始化 contentInset
|
// 初始化 contentInset
|
||||||
self.contentBottomInset = 0;
|
self.contentBottomInset = 0;
|
||||||
[self updateContentBottomInset:self.contentBottomInset];
|
[self updateContentBottomInset:self.contentBottomInset];
|
||||||
@@ -136,6 +144,32 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Long Press
|
||||||
|
|
||||||
|
- (void)handleMessageLongPress:(UILongPressGestureRecognizer *)gesture {
|
||||||
|
if (gesture.state != UIGestureRecognizerStateBegan) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGPoint point = [gesture locationInView:self.tableView];
|
||||||
|
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:point];
|
||||||
|
if (!indexPath || indexPath.row >= self.messages.count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBAiChatMessage *message = self.messages[indexPath.row];
|
||||||
|
if (!message || message.isLoading || message.type == KBAiChatMessageTypeTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||||
|
CGRect cellRect = cell ? [cell convertRect:cell.bounds toView:nil] : CGRectZero;
|
||||||
|
|
||||||
|
if ([self.delegate respondsToSelector:@selector(chatTableView:didLongPressMessage:sourceRect:)]) {
|
||||||
|
[self.delegate chatTableView:self didLongPressMessage:message sourceRect:cellRect];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Public Methods
|
#pragma mark - Public Methods
|
||||||
|
|
||||||
- (void)setInverted:(BOOL)inverted {
|
- (void)setInverted:(BOOL)inverted {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
self.messageLabel.numberOfLines = 0;
|
self.messageLabel.numberOfLines = 0;
|
||||||
self.messageLabel.font = [UIFont systemFontOfSize:16];
|
self.messageLabel.font = [UIFont systemFontOfSize:16];
|
||||||
self.messageLabel.textColor = [UIColor blackColor];
|
self.messageLabel.textColor = [UIColor blackColor];
|
||||||
|
self.messageLabel.textAlignment = NSTextAlignmentLeft;
|
||||||
[self.bubbleView addSubview:self.messageLabel];
|
[self.bubbleView addSubview:self.messageLabel];
|
||||||
|
|
||||||
// 加载指示器
|
// 加载指示器
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
|
|
||||||
- (void)configureWithMessage:(KBAiChatMessage *)message {
|
- (void)configureWithMessage:(KBAiChatMessage *)message {
|
||||||
self.messageLabel.text = message.text;
|
self.messageLabel.text = message.text;
|
||||||
|
[self updateMessageAlignmentForText:message.text];
|
||||||
|
|
||||||
if (message.isLoading) {
|
if (message.isLoading) {
|
||||||
self.bubbleView.hidden = YES;
|
self.bubbleView.hidden = YES;
|
||||||
@@ -87,4 +89,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)updateMessageAlignmentForText:(NSString *)text {
|
||||||
|
if (self.messageLabel.hidden || text.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([text rangeOfString:@"\n"].location != NSNotFound) {
|
||||||
|
self.messageLabel.textAlignment = NSTextAlignmentLeft;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGSize singleLineSize = [text sizeWithAttributes:@{NSFontAttributeName: self.messageLabel.font}];
|
||||||
|
CGFloat minBubbleWidth = 50.0;
|
||||||
|
CGFloat padding = 24.0;
|
||||||
|
if (singleLineSize.width + padding <= minBubbleWidth + 0.5) {
|
||||||
|
self.messageLabel.textAlignment = NSTextAlignmentCenter;
|
||||||
|
} else {
|
||||||
|
self.messageLabel.textAlignment = NSTextAlignmentLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
#import "KBImagePositionButton.h"
|
#import "KBImagePositionButton.h"
|
||||||
#import "KBAICommentView.h"
|
#import "KBAICommentView.h"
|
||||||
#import "KBAIChatMessageCacheManager.h"
|
#import "KBAIChatMessageCacheManager.h"
|
||||||
|
#import "KBChatMessageActionPopView.h"
|
||||||
|
#import "AIReportVC.h"
|
||||||
|
#import "KBHUD.h"
|
||||||
#import <Masonry/Masonry.h>
|
#import <Masonry/Masonry.h>
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
#import <LSTPopView/LSTPopView.h>
|
#import <LSTPopView/LSTPopView.h>
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
/// 聊天会话被重置的通知
|
/// 聊天会话被重置的通知
|
||||||
static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification";
|
static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification";
|
||||||
|
|
||||||
@interface KBPersonaChatCell () <KBChatTableViewDelegate>
|
@interface KBPersonaChatCell () <KBChatTableViewDelegate, KBChatMessageActionPopViewDelegate>
|
||||||
|
|
||||||
/// 背景图
|
/// 背景图
|
||||||
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
||||||
@@ -65,6 +68,9 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
@property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio;
|
@property (nonatomic, assign) BOOL shouldAutoPlayPrologueAudio;
|
||||||
@property (nonatomic, assign) BOOL hasPlayedPrologueAudio;
|
@property (nonatomic, assign) BOOL hasPlayedPrologueAudio;
|
||||||
@property (nonatomic, assign) BOOL shouldShowOpeningMessage;
|
@property (nonatomic, assign) BOOL shouldShowOpeningMessage;
|
||||||
|
@property (nonatomic, weak) LSTPopView *messageActionPopView;
|
||||||
|
@property (nonatomic, strong) KBAiChatMessage *selectedActionMessage;
|
||||||
|
@property (nonatomic, strong) UIControl *messageActionMaskView;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -898,6 +904,126 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
[self loadMoreHistory];
|
[self loadMoreHistory];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)chatTableView:(KBChatTableView *)chatView
|
||||||
|
didLongPressMessage:(KBAiChatMessage *)message
|
||||||
|
sourceRect:(CGRect)sourceRect {
|
||||||
|
[self showMessageActionPopForMessage:message sourceRect:sourceRect];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - KBChatMessageActionPopViewDelegate
|
||||||
|
|
||||||
|
- (void)chatMessageActionPopView:(KBChatMessageActionPopView *)view
|
||||||
|
didSelectAction:(KBChatMessageActionType)action {
|
||||||
|
[self dismissMessageActionPop];
|
||||||
|
|
||||||
|
KBAiChatMessage *message = self.selectedActionMessage;
|
||||||
|
self.selectedActionMessage = nil;
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case KBChatMessageActionTypeCopy: {
|
||||||
|
if (message.text.length > 0) {
|
||||||
|
[UIPasteboard generalPasteboard].string = message.text;
|
||||||
|
[KBHUD showSuccess:KBLocalized(@"复制成功")];
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case KBChatMessageActionTypeDelete: {
|
||||||
|
NSInteger idx = [self.messages indexOfObjectIdenticalTo:message];
|
||||||
|
if (idx != NSNotFound) {
|
||||||
|
[self.messages removeObjectAtIndex:idx];
|
||||||
|
[self.chatView reloadWithMessages:self.messages
|
||||||
|
keepOffset:YES
|
||||||
|
scrollToBottom:NO];
|
||||||
|
if (self.persona.personaId > 0) {
|
||||||
|
if (self.messages.count > 0) {
|
||||||
|
[[KBAIChatMessageCacheManager shared] saveMessages:self.messages
|
||||||
|
forCompanionId:self.persona.personaId];
|
||||||
|
} else {
|
||||||
|
[[KBAIChatMessageCacheManager shared] clearMessagesForCompanionId:self.persona.personaId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case KBChatMessageActionTypeReport: {
|
||||||
|
if (self.persona.personaId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AIReportVC *vc = [[AIReportVC alloc] init];
|
||||||
|
vc.personaId = self.persona.personaId;
|
||||||
|
[KB_CURRENT_NAV pushViewController:vc animated:YES];
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Message Action Pop
|
||||||
|
|
||||||
|
- (void)showMessageActionPopForMessage:(KBAiChatMessage *)message
|
||||||
|
sourceRect:(CGRect)sourceRect {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self dismissMessageActionPop];
|
||||||
|
|
||||||
|
self.selectedActionMessage = message;
|
||||||
|
CGFloat width = 240;
|
||||||
|
CGFloat height = 156;
|
||||||
|
KBChatMessageActionPopView *content = [[KBChatMessageActionPopView alloc]
|
||||||
|
initWithFrame:CGRectMake(0, 0, width, height)];
|
||||||
|
content.delegate = self;
|
||||||
|
|
||||||
|
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||||
|
if (!window) {
|
||||||
|
window = [UIApplication sharedApplication].windows.firstObject;
|
||||||
|
}
|
||||||
|
if (!window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIControl *mask = [[UIControl alloc] initWithFrame:window.bounds];
|
||||||
|
[mask addTarget:self action:@selector(dismissMessageActionPop) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[window addSubview:mask];
|
||||||
|
self.messageActionMaskView = mask;
|
||||||
|
|
||||||
|
BOOL isUserMessage = (message.type == KBAiChatMessageTypeUser);
|
||||||
|
CGFloat margin = 12.0;
|
||||||
|
CGFloat spacing = 8.0;
|
||||||
|
CGFloat topSafe = 0.0;
|
||||||
|
CGFloat bottomSafe = 0.0;
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
topSafe = window.safeAreaInsets.top;
|
||||||
|
bottomSafe = window.safeAreaInsets.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGFloat x = isUserMessage ? CGRectGetMaxX(sourceRect) - width : CGRectGetMinX(sourceRect);
|
||||||
|
x = MAX(margin, MIN(x, CGRectGetWidth(window.bounds) - width - margin));
|
||||||
|
|
||||||
|
CGFloat y = CGRectGetMinY(sourceRect) - height - spacing;
|
||||||
|
if (y < topSafe + margin) {
|
||||||
|
y = CGRectGetMaxY(sourceRect) + spacing;
|
||||||
|
}
|
||||||
|
if (y + height > CGRectGetHeight(window.bounds) - bottomSafe - margin) {
|
||||||
|
y = MAX(topSafe + margin, CGRectGetHeight(window.bounds) - bottomSafe - margin - height);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.frame = CGRectMake(x, y, width, height);
|
||||||
|
[mask addSubview:content];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dismissMessageActionPop {
|
||||||
|
if (self.messageActionPopView) {
|
||||||
|
[self.messageActionPopView dismiss];
|
||||||
|
self.messageActionPopView = nil;
|
||||||
|
}
|
||||||
|
if (self.messageActionMaskView) {
|
||||||
|
[self.messageActionMaskView removeFromSuperview];
|
||||||
|
self.messageActionMaskView = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy Load
|
#pragma mark - Lazy Load
|
||||||
|
|
||||||
- (UIImageView *)backgroundImageView {
|
- (UIImageView *)backgroundImageView {
|
||||||
|
|||||||
51
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h
Normal 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
|
||||||
527
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
//
|
||||||
|
// 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.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 () <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) BaseTableView *tableView;
|
||||||
|
@property (nonatomic, strong) UIView *searchResultContainer;
|
||||||
|
@property (nonatomic, strong) BaseTableView *searchResultTableView;
|
||||||
|
|
||||||
|
@property (nonatomic, strong) NSArray<KBPersonaModel *> *personas;
|
||||||
|
@property (nonatomic, strong) NSArray<KBPersonaModel *> *displayPersonas;
|
||||||
|
@property (nonatomic, strong) NSArray<KBPersonaModel *> *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<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;
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -194,6 +206,14 @@
|
|||||||
make.right.equalTo(self.view).offset(-16);
|
make.right.equalTo(self.view).offset(-16);
|
||||||
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];
|
||||||
@@ -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,14 +687,51 @@
|
|||||||
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
|
||||||
&& !self.isVoiceProcessing;
|
&& !self.isVoiceProcessing;
|
||||||
self.collectionView.scrollEnabled = shouldEnable;
|
self.collectionView.scrollEnabled = shouldEnable;
|
||||||
self.collectionView.panGestureRecognizer.enabled = shouldEnable;
|
self.collectionView.panGestureRecognizer.enabled = shouldEnable;
|
||||||
|
self.collectionView.userInteractionEnabled = shouldEnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)updateChatViewBottomInset {
|
- (void)updateChatViewBottomInset {
|
||||||
@@ -765,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];
|
||||||
@@ -806,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 {
|
||||||
@@ -813,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]];
|
||||||
|
|||||||