From 2b749cd2b082b7bd7f68e7a7bf00bea498c40404 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Tue, 27 Jan 2026 16:28:17 +0800 Subject: [PATCH] 1 --- Shared/KBAPI.h | 1 + keyBoard.xcodeproj/project.pbxproj | 18 +- keyBoard/Assets.xcassets/AI/Contents.json | 6 + .../AI/ai_jianpan_icon.imageset/Contents.json | 22 + .../ai_jianpan_icon@2x.png | Bin 0 -> 1955 bytes .../ai_jianpan_icon@3x.png | Bin 0 -> 3502 bytes .../ai_luyining_icon.imageset/Contents.json | 22 + .../ai_luyining_icon@2x.png | Bin 0 -> 9036 bytes .../ai_luyining_icon@3x.png | Bin 0 -> 18473 bytes .../ai_maikefeng_icon.imageset/Contents.json | 22 + .../ai_maikefeng_icon@2x.png | Bin 0 -> 1588 bytes .../ai_maikefeng_icon@3x.png | Bin 0 -> 2808 bytes keyBoard/Class/AiTalk/V/KBAiRecordButton.h | 6 + keyBoard/Class/AiTalk/V/KBAiRecordButton.m | 44 +- keyBoard/Class/AiTalk/V/KBChatLimitPopView.h | 28 ++ keyBoard/Class/AiTalk/V/KBChatLimitPopView.m | 157 +++++++ keyBoard/Class/AiTalk/V/KBChatTableView.h | 3 + keyBoard/Class/AiTalk/V/KBChatTableView.m | 16 +- keyBoard/Class/AiTalk/V/KBPersonaChatCell.h | 3 + keyBoard/Class/AiTalk/V/KBPersonaChatCell.m | 4 + keyBoard/Class/AiTalk/V/KBVoiceInputBar.h | 18 + keyBoard/Class/AiTalk/V/KBVoiceInputBar.m | 389 +++++++++++++++--- keyBoard/Class/AiTalk/VC/KBAIHomeVC.m | 331 ++++++++++++--- keyBoard/Class/AiTalk/VC/KBAiMainVC.m | 60 ++- keyBoard/Class/AiTalk/VM/AiVM.h | 21 + keyBoard/Class/AiTalk/VM/AiVM.m | 49 ++- 26 files changed, 1092 insertions(+), 128 deletions(-) create mode 100644 keyBoard/Assets.xcassets/AI/Contents.json create mode 100644 keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json create mode 100644 keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@2x.png create mode 100644 keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@3x.png create mode 100644 keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json create mode 100644 keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@2x.png create mode 100644 keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@3x.png create mode 100644 keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json create mode 100644 keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@2x.png create mode 100644 keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@3x.png create mode 100644 keyBoard/Class/AiTalk/V/KBChatLimitPopView.h create mode 100644 keyBoard/Class/AiTalk/V/KBChatLimitPopView.m diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index 1173ce0..29ebc86 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -73,6 +73,7 @@ #define API_AI_CHAT_SYNC @"/chat/sync" // 同步对话 #define API_AI_CHAT_MESSAGE @"/chat/message" // 文本润色 #define API_AI_AUDIO_UPLOAD @"/chat/audio/upload" // 语音上传(替换为后端真实路径) +#define API_AI_SPEECH_TRANSCRIBE @"/speech/transcribe" // 语音转文字 diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 1848db9..c87673b 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -143,6 +143,7 @@ 048FFD182F2763A5005D62AE /* KBVoiceInputBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD172F2763A5005D62AE /* KBVoiceInputBar.m */; }; 048FFD1D2F277486005D62AE /* KBChatHistoryPageModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD1C2F277486005D62AE /* KBChatHistoryPageModel.m */; }; 048FFD1E2F277486005D62AE /* KBChatHistoryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD1A2F277486005D62AE /* KBChatHistoryModel.m */; }; + 048FFD242F28A836005D62AE /* KBChatLimitPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */; }; 0498BD622EDFFC12006CC1D5 /* KBMyVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */; }; 0498BD652EE0116D006CC1D5 /* KBEmailLoginVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD642EE0116D006CC1D5 /* KBEmailLoginVC.m */; }; 0498BD682EE01180006CC1D5 /* KBEmailRegistVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD672EE01180006CC1D5 /* KBEmailRegistVC.m */; }; @@ -208,8 +209,6 @@ 04E038E32F20E500002CA5A0 /* deepgramAPI.md in Resources */ = {isa = PBXBuildFile; fileRef = 04E038E22F20E500002CA5A0 /* deepgramAPI.md */; }; 04E038E82F20E877002CA5A0 /* DeepgramWebSocketClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */; }; 04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */; }; - 04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */; }; - 04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */; }; 04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; }; 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */; }; 04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039482F236E75002CA5A0 /* KBChatTimeCell.m */; }; @@ -217,6 +216,8 @@ 04E0394E2F236E75002CA5A0 /* KBChatTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039452F236E75002CA5A0 /* KBChatTableView.m */; }; 04E0394F2F236E75002CA5A0 /* KBChatTableView_Usage.md in Resources */ = {isa = PBXBuildFile; fileRef = 04E039462F236E75002CA5A0 /* KBChatTableView_Usage.md */; }; 04E039522F2387D2002CA5A0 /* KBAiChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */; }; + 04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */; }; + 04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */; }; 04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161812F10E6470022C23B /* normal_hei_them.zip */; }; 04E161842F10E6470022C23B /* normal_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161822F10E6470022C23B /* normal_them.zip */; }; 04FC95672EB0546C007BD342 /* KBKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95652EB0546C007BD342 /* KBKey.m */; }; @@ -541,6 +542,8 @@ 048FFD1A2F277486005D62AE /* KBChatHistoryModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatHistoryModel.m; sourceTree = ""; }; 048FFD1B2F277486005D62AE /* KBChatHistoryPageModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatHistoryPageModel.h; sourceTree = ""; }; 048FFD1C2F277486005D62AE /* KBChatHistoryPageModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatHistoryPageModel.m; sourceTree = ""; }; + 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatLimitPopView.h; sourceTree = ""; }; + 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatLimitPopView.m; sourceTree = ""; }; 0498BD5E2EDF2157006CC1D5 /* KBBizCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBizCode.h; sourceTree = ""; }; 0498BD602EDFFC12006CC1D5 /* KBMyVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyVM.h; sourceTree = ""; }; 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyVM.m; sourceTree = ""; }; @@ -652,10 +655,6 @@ 04E038E22F20E500002CA5A0 /* deepgramAPI.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = deepgramAPI.md; sourceTree = ""; }; 04E038E42F20E877002CA5A0 /* DeepgramStreamingManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeepgramStreamingManager.h; sourceTree = ""; }; 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramStreamingManager.m; sourceTree = ""; }; - 04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = ""; }; - 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceToTextManager.m; sourceTree = ""; }; - 04E0B2002F300002002CA5A0 /* KBVoiceRecordManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceRecordManager.h; sourceTree = ""; }; - 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceRecordManager.m; sourceTree = ""; }; 04E038E62F20E877002CA5A0 /* DeepgramWebSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeepgramWebSocketClient.h; sourceTree = ""; }; 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramWebSocketClient.m; sourceTree = ""; }; 04E038ED2F21F0EC002CA5A0 /* AiVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AiVM.h; sourceTree = ""; }; @@ -671,6 +670,10 @@ 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatUserMessageCell.m; sourceTree = ""; }; 04E039502F2387D2002CA5A0 /* KBAiChatMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAiChatMessage.h; sourceTree = ""; }; 04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAiChatMessage.m; sourceTree = ""; }; + 04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = ""; }; + 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceToTextManager.m; sourceTree = ""; }; + 04E0B2002F300002002CA5A0 /* KBVoiceRecordManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceRecordManager.h; sourceTree = ""; }; + 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceRecordManager.m; sourceTree = ""; }; 04E161812F10E6470022C23B /* normal_hei_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_hei_them.zip; sourceTree = ""; }; 04E161822F10E6470022C23B /* normal_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_them.zip; sourceTree = ""; }; 04FC953A2EAFAE56007BD342 /* KeyBoardPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyBoardPrefixHeader.pch; sourceTree = ""; }; @@ -1036,6 +1039,8 @@ 048FFD132F274342005D62AE /* KBPersonaChatCell.m */, 048FFD162F2763A5005D62AE /* KBVoiceInputBar.h */, 048FFD172F2763A5005D62AE /* KBVoiceInputBar.m */, + 048FFD222F28A836005D62AE /* KBChatLimitPopView.h */, + 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */, ); path = V; sourceTree = ""; @@ -2287,6 +2292,7 @@ 04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */, 04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */, 048FFD0B2F273BFC005D62AE /* KBAIHomeVC.m in Sources */, + 048FFD242F28A836005D62AE /* KBChatLimitPopView.m in Sources */, 046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */, 046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */, 04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */, diff --git a/keyBoard/Assets.xcassets/AI/Contents.json b/keyBoard/Assets.xcassets/AI/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json new file mode 100644 index 0000000..6366814 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ai_jianpan_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ai_jianpan_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@2x.png b/keyBoard/Assets.xcassets/AI/ai_jianpan_icon.imageset/ai_jianpan_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..812be137bed3fe7731aee661fa7e46967529ba38 GIT binary patch literal 1955 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 literal 0 HcmV?d00001 diff --git a/keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json new file mode 100644 index 0000000..24ff511 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ai_luyining_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ai_luyining_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@2x.png b/keyBoard/Assets.xcassets/AI/ai_luyining_icon.imageset/ai_luyining_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..916acef07d680e5d2781f66a5dce89765babc1ed GIT binary patch literal 9036 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∋$)Kxt;lH5n0JYlJTwm8 z3#-3t&66QHIW{m%z=>}aeul3>-m&42Vl7!q4}wV) zEaZz)4m+-T+>!h5ayH^6o@>ubwCkGtzx8h>^^=zK+R?1mqa*G^0CN=WOCTr}%=#71XTCkZT@yRW<8i#>FKSma5$x$E*>clI?27xXi zFSaJhlp0P(IiInOZ*Pn*abdBe7FgX*A^q2Ul)WKlw~p+n@r`iG(lLhO|&*jm={|Cp8uiOGyraHP*y5KH-$OvW{LL1`k1 z?98@v_hbk1t+XgMJ2B*8TMZwHs%gTguZ*tb{651gyHPfLC-4(;Bep}WOZbIvS)ka! zI68<9VNWbG5-&FL@Q9ar^BEXDgs<^AM9kf2<`P%b)5nr{TqP63)ROr{Zf1ii@uNS) zmvWdmJ9aULQXlz>Sc3usLr}>+Pwr2;obLQdeHlmk|7F0WYwo(EW1mRzd8<3&!Qv8>@Bf8u{ zVkVvcG2e~1KTtgq*M8XrzXawF_sK%XU4Gq_lh-GkQ*Ne^J zVdw71T`_j5!Q|{*ir5~0Epub1c;w_I#*V!hSw!p}vB@iUN8%x8=E(vFCXEeYZ)>~p z)_C-0t`Oklw~nV^*RsG`IaXW>nj|xlmj=b!OQ=5cuu7%2jjCXQ3c7uyw%Q$2ax8Y;Pz7RRW za3+R`(IA0Ibs;N?B1TTLDK+{s551T#b_B`W1nn=_qg}tEukc{jugv4h{_meX*j6V| zATJ?}0yLR*a8=UBYh^A_PwXM~o?c0A^U*SP!U!e!^ zYx}!DS(^v$-Q54Uxzwnz z4qg|JNrPB`2BLAdHR2h+1t(2-H5${#i$UTVq|xSNzUiPuBct&l3mkWZ1HspcKdq#p zGpNJ~4FGaxCl+%z>XkMy1Sf1=;)=7Byx1I~p0+Fa3AWBGANcXZef*d8l0lHqJKN_; zUx9G-!*2Rf+xMMHXIe$0RxE&&v`rxsiB1&mif%^Vl_DBgLjq$8az+cgp^U-NtYF6W z?C4j-UNP1}1&_^OQou6y&&PmN=GOu02Nn3XG778*C0=w}=yOI6Ih@Ux6951Qu1Q2e zR6<~5bQUhjFVeW?V)D(MG)&|mYEV9FqaT1(u4^*T#LYOPt24R$+as?zz$0A8&Z-6S++k%03$HGF4@Q&_uJQv9pWD(%i~u_yQa1%UBwZ zt4U(tcKG71YTyiElfZ$+pYZTC^wAoLX=r+f!%t$2Z-EKTY^OXa$8C54PAB5y}tANFn-l%`>H+Hj#u-I z&R%)`3CX`hvg7S>l4y3LIW?$_tXFc1OmQK0q`{f7MFtbzpp2ecxppMcHjIOd9J0*D zX!xd22Ha$c4Z($T!dLhy+CeRIOYnu>0t9tQiQVC6Y(zm!JcAk-a?_!>_+i&L;u9u{ z2Gj9pz|YJTPH2dA(B02mdHc@${iVNl(wA{$rY{5br6E`I4L{Y|<|CV?-?rMpW|L}- z)Gdv%)dr)M1yl{Y({M;UR?N6Ge}zYy*TEQ{Qgc@tKA~wLK3f`O38~gQzQgx{Ydnq{ zoD!4fjo4V5w8kdB(G-M^tUU0GT*|Rr6C;$|M6AKbjF0d(wJ0wkEUKlU5wR?e5^vJ_ z?LF;+U;b)8vxxF}XKMI7#aAm_**E>{m8Tm%*loVQ%l}5glu(q_Nq=UX8AUp8ur*ADFyX2s;}<6&v!d@sJ@1m;AodY%n|ZvfBz@0F#W&@e1Ui?tKMw4?MBf}rum`b#YFH8nxODq8u-24>OK+TPPfsI=qb)x_qvJYp3I zEM!oYxE5rIZybhY+*t*_uy&~6JD6fDa2x2l%m%@W5gm+e8H?f%)oS|3vLDwsw|;29 zfAozpU6zzST$b@i>?8Nz;_NH7zxcJO)t#8|cWLc`jg1w%yl9Z22@({!FCz=vXx2vl zs}DX)tfEmJDhZp$g3idHl{~GaEEeVl=RpY3fpYv4XM8h(o->a+p&JBF#FY6OiFpmq z@fp72DhYkQD~zjM2g>zW*VeIn+qqBNzyGWMWmsRqk=nij_*W0jzf(BEKlHwDn11l* z`)b#X>;n$^(0W#elgiSdj;G0D=an$+sql^`w*p(3{_^CE}tLa?m)$_c%>AvZ1kKeoZ z$KGfEuY7d^;<7*6Wk1EgpW%voZuZW%pVjWxT04#n{&?E+rY7fefcDZHx^8x}+3^c? z4cQqBUJxVpEVgNC6EchF(QI)bj~KAn1y;zBM4a%p5rZowPIL>Q#%4B;c#*((P)+YC z`;=+g_@O_Xw(fM;?jQf6|85Y^%k*rQ5dAV&-{KaxSRL+Lo^?cf)8i)luD0$lY1iwU zz5c(;O=>i(S+XswK^a3cQkon-4WES(vMxG0Y)9Q0cg}(dt#Oqxawtm#8x-14%>gbTUtA;<0t#!3VJVx8HI54{D zfW7OJ_VkZ-d$`oDv)6aN{b3u&y?UR2{Dc3Qk3KT`qUYCr!M-r7%lUTa9Om0TW}iNH z(+%~TTGPForeC42f6(N2G<*581PTo*Sz0KcXL&S^h1FQtmW7gJR>Sbbkd?!Xb%9jx zko=-5|Ei^XTmHd4I{Q2K_>QOS{SSUotIObMmqE<0Nl5$nt-ZeEKkfHzU%FR+d(xKr z&&s=xU2SjuK%eUt^6Gx9*E8()YFT-m{*YVy7`jb~vYa%y*q=c6UX|a{s{MiGXYfVS z{aW`Ax8@tF?K<{AJNWZ^+dEI#I{vQAU0RR8}VAjh3000I_ cL_t&o0Q*6Zln{QvcmMzZ07*qoM6N<$f`3jan*aa+ literal 0 HcmV?d00001 diff --git a/keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json b/keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json new file mode 100644 index 0000000..52e1ea2 --- /dev/null +++ b/keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ai_maikefeng_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ai_maikefeng_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@2x.png b/keyBoard/Assets.xcassets/AI/ai_maikefeng_icon.imageset/ai_maikefeng_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..71a3a8da6b731bf86298541adfed0b0664dc23c3 GIT binary patch literal 1588 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 KMNUMnLSTZc*LbS{ literal 0 HcmV?d00001 diff --git a/keyBoard/Class/AiTalk/V/KBAiRecordButton.h b/keyBoard/Class/AiTalk/V/KBAiRecordButton.h index 35938d0..b6fb2dc 100644 --- a/keyBoard/Class/AiTalk/V/KBAiRecordButton.h +++ b/keyBoard/Class/AiTalk/V/KBAiRecordButton.h @@ -47,6 +47,12 @@ typedef NS_ENUM(NSInteger, KBAiRecordButtonState) { /// 主色调 @property(nonatomic, strong) UIColor *tintColor; +/// 正常状态左侧图标 +@property(nonatomic, strong, nullable) UIImage *normalIconImage; + +/// 录音中状态图标(居中显示) +@property(nonatomic, strong, nullable) UIImage *recordingIconImage; + /// 更新音量(用于波形动画) /// @param rms 音量 RMS 值 (0.0 - 1.0) - (void)updateVolumeRMS:(float)rms; diff --git a/keyBoard/Class/AiTalk/V/KBAiRecordButton.m b/keyBoard/Class/AiTalk/V/KBAiRecordButton.m index 3d24827..f608e3c 100644 --- a/keyBoard/Class/AiTalk/V/KBAiRecordButton.m +++ b/keyBoard/Class/AiTalk/V/KBAiRecordButton.m @@ -14,6 +14,7 @@ @property(nonatomic, strong) UILabel *titleLabel; @property(nonatomic, strong) KBAiWaveformView *waveformView; @property(nonatomic, strong) UIImageView *micIconView; +@property(nonatomic, strong) UIImageView *recordingIconView; @property(nonatomic, assign) BOOL isPressing; @end @@ -50,7 +51,7 @@ self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.backgroundView]; - // 麦克风图标 + // 麦克风图标(正常态左侧) self.micIconView = [[UIImageView alloc] init]; self.micIconView.image = [UIImage systemImageNamed:@"mic.fill"]; self.micIconView.tintColor = self.tintColor; @@ -58,6 +59,13 @@ self.micIconView.translatesAutoresizingMaskIntoConstraints = NO; [self.backgroundView addSubview:self.micIconView]; + // 录音中图标(居中) + self.recordingIconView = [[UIImageView alloc] init]; + self.recordingIconView.contentMode = UIViewContentModeScaleAspectFit; + self.recordingIconView.translatesAutoresizingMaskIntoConstraints = NO; + self.recordingIconView.hidden = YES; + [self.backgroundView addSubview:self.recordingIconView]; + // 标题标签 self.titleLabel = [[UILabel alloc] init]; self.titleLabel.text = self.normalTitle; @@ -104,6 +112,13 @@ constraintEqualToAnchor:self.backgroundView.centerYAnchor], [self.waveformView.widthAnchor constraintEqualToConstant:60], [self.waveformView.heightAnchor constraintEqualToConstant:30], + + [self.recordingIconView.centerXAnchor + constraintEqualToAnchor:self.backgroundView.centerXAnchor], + [self.recordingIconView.centerYAnchor + constraintEqualToAnchor:self.backgroundView.centerYAnchor], + [self.recordingIconView.widthAnchor constraintEqualToConstant:36], + [self.recordingIconView.heightAnchor constraintEqualToConstant:36], ]]; // 添加手势 @@ -131,6 +146,18 @@ self.waveformView.waveColor = tintColor; } +- (void)setNormalIconImage:(UIImage *)normalIconImage { + _normalIconImage = normalIconImage; + if (normalIconImage) { + self.micIconView.image = normalIconImage; + } +} + +- (void)setRecordingIconImage:(UIImage *)recordingIconImage { + _recordingIconImage = recordingIconImage; + self.recordingIconView.image = recordingIconImage; +} + #pragma mark - Public Methods - (void)updateVolumeRMS:(float)rms { @@ -144,18 +171,21 @@ case KBAiRecordButtonStateNormal: self.titleLabel.text = self.normalTitle; self.backgroundView.backgroundColor = [UIColor systemGray6Color]; - self.micIconView.alpha = 1; + self.micIconView.hidden = NO; + self.titleLabel.hidden = NO; + self.recordingIconView.hidden = YES; self.waveformView.alpha = 0; [self.waveformView stopAnimation]; break; case KBAiRecordButtonStateRecording: self.titleLabel.text = self.recordingTitle; - self.backgroundView.backgroundColor = - [self.tintColor colorWithAlphaComponent:0.15]; - self.micIconView.alpha = 1; - self.waveformView.alpha = 1; - [self.waveformView startIdleAnimation]; + self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6]; + self.micIconView.hidden = YES; + self.titleLabel.hidden = YES; + self.recordingIconView.hidden = NO; + self.waveformView.alpha = 0; + [self.waveformView stopAnimation]; break; case KBAiRecordButtonStateDisabled: diff --git a/keyBoard/Class/AiTalk/V/KBChatLimitPopView.h b/keyBoard/Class/AiTalk/V/KBChatLimitPopView.h new file mode 100644 index 0000000..fffa3d1 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBChatLimitPopView.h @@ -0,0 +1,28 @@ +// +// KBChatLimitPopView.h +// keyBoard +// +// Created by Codex on 2026/1/27. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class KBChatLimitPopView; + +@protocol KBChatLimitPopViewDelegate +@optional +- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view; +- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view; +@end + +/// 聊天次数用尽提示弹窗内容视图(配合 LSTPopView 使用) +@interface KBChatLimitPopView : UIView + +@property (nonatomic, weak) id delegate; +@property (nonatomic, copy) NSString *message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBChatLimitPopView.m b/keyBoard/Class/AiTalk/V/KBChatLimitPopView.m new file mode 100644 index 0000000..1280283 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBChatLimitPopView.m @@ -0,0 +1,157 @@ +// +// KBChatLimitPopView.m +// keyBoard +// +// Created by Codex on 2026/1/27. +// + +#import "KBChatLimitPopView.h" +#import + +@interface KBChatLimitPopView () +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *messageLabel; +@property (nonatomic, strong) UIButton *cancelButton; +@property (nonatomic, strong) UIButton *rechargeButton; +@property (nonatomic, strong) UIView *buttonDivider; +@end + +@implementation KBChatLimitPopView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.backgroundColor = [UIColor whiteColor]; + self.layer.cornerRadius = 16.0; + self.layer.masksToBounds = YES; + [self setupUI]; + } + return self; +} + +#pragma mark - UI + +- (void)setupUI { + [self addSubview:self.titleLabel]; + [self addSubview:self.messageLabel]; + [self addSubview:self.buttonDivider]; + [self addSubview:self.cancelButton]; + [self addSubview:self.rechargeButton]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self).offset(20); + make.left.equalTo(self).offset(20); + make.right.equalTo(self).offset(-20); + }]; + + [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.titleLabel.mas_bottom).offset(8); + make.left.equalTo(self).offset(20); + make.right.equalTo(self).offset(-20); + }]; + + [self.buttonDivider mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self); + make.height.mas_equalTo(1); + make.top.greaterThanOrEqualTo(self.messageLabel.mas_bottom).offset(16); + make.bottom.equalTo(self).offset(-48); + }]; + + [self.cancelButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.bottom.equalTo(self); + make.top.equalTo(self.buttonDivider.mas_bottom); + make.right.equalTo(self.mas_centerX); + }]; + + [self.rechargeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.bottom.equalTo(self); + make.top.equalTo(self.buttonDivider.mas_bottom); + make.left.equalTo(self.mas_centerX); + }]; + + UIView *verticalLine = [[UIView alloc] init]; + verticalLine.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + [self addSubview:verticalLine]; + [verticalLine mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self); + make.top.equalTo(self.buttonDivider.mas_bottom); + make.bottom.equalTo(self); + make.width.mas_equalTo(1); + }]; +} + +#pragma mark - Actions + +- (void)onTapCancel { + if ([self.delegate respondsToSelector:@selector(chatLimitPopViewDidTapCancel:)]) { + [self.delegate chatLimitPopViewDidTapCancel:self]; + } +} + +- (void)onTapRecharge { + if ([self.delegate respondsToSelector:@selector(chatLimitPopViewDidTapRecharge:)]) { + [self.delegate chatLimitPopViewDidTapRecharge:self]; + } +} + +#pragma mark - Setter + +- (void)setMessage:(NSString *)message { + _message = [message copy]; + self.messageLabel.text = _message.length > 0 ? _message : @""; +} + +#pragma mark - Lazy + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.text = KBLocalized(@"提示"); + _titleLabel.font = [UIFont boldSystemFontOfSize:18]; + _titleLabel.textColor = [UIColor blackColor]; + _titleLabel.textAlignment = NSTextAlignmentCenter; + } + return _titleLabel; +} + +- (UILabel *)messageLabel { + if (!_messageLabel) { + _messageLabel = [[UILabel alloc] init]; + _messageLabel.font = [UIFont systemFontOfSize:14]; + _messageLabel.textColor = [UIColor colorWithWhite:0.2 alpha:1.0]; + _messageLabel.textAlignment = NSTextAlignmentCenter; + _messageLabel.numberOfLines = 0; + } + return _messageLabel; +} + +- (UIButton *)cancelButton { + if (!_cancelButton) { + _cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_cancelButton setTitle:KBLocalized(@"取消") forState:UIControlStateNormal]; + _cancelButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; + [_cancelButton setTitleColor:[UIColor colorWithWhite:0.2 alpha:1.0] forState:UIControlStateNormal]; + [_cancelButton addTarget:self action:@selector(onTapCancel) forControlEvents:UIControlEventTouchUpInside]; + } + return _cancelButton; +} + +- (UIButton *)rechargeButton { + if (!_rechargeButton) { + _rechargeButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_rechargeButton setTitle:KBLocalized(@"去充值") forState:UIControlStateNormal]; + _rechargeButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + [_rechargeButton setTitleColor:[UIColor colorWithRed:0.28 green:0.45 blue:0.94 alpha:1.0] forState:UIControlStateNormal]; + [_rechargeButton addTarget:self action:@selector(onTapRecharge) forControlEvents:UIControlEventTouchUpInside]; + } + return _rechargeButton; +} + +- (UIView *)buttonDivider { + if (!_buttonDivider) { + _buttonDivider = [[UIView alloc] init]; + _buttonDivider.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + } + return _buttonDivider; +} + +@end diff --git a/keyBoard/Class/AiTalk/V/KBChatTableView.h b/keyBoard/Class/AiTalk/V/KBChatTableView.h index d47a58b..2e4f6d4 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTableView.h +++ b/keyBoard/Class/AiTalk/V/KBChatTableView.h @@ -57,6 +57,9 @@ NS_ASSUME_NONNULL_BEGIN /// 重置无更多数据状态 - (void)resetNoMoreData; +/// 更新底部内容 inset(用于避开输入栏/键盘) +- (void)updateContentBottomInset:(CGFloat)bottomInset; + /// 添加自定义消息(可用于历史消息或打字机) - (void)addMessage:(KBAiChatMessage *)message autoScroll:(BOOL)autoScroll; diff --git a/keyBoard/Class/AiTalk/V/KBChatTableView.m b/keyBoard/Class/AiTalk/V/KBChatTableView.m index ce1178f..5dcaedd 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/KBChatTableView.m @@ -30,6 +30,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 @property (nonatomic, strong) NSIndexPath *playingCellIndexPath; @property (nonatomic, strong) AiVM *aiVM; @property (nonatomic, assign) BOOL hasMoreData; +@property (nonatomic, assign) CGFloat contentBottomInset; @end @@ -78,11 +79,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 // 布局 [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { -// make.edges.equalTo(self); - make.top.left.right.equalTo(self); - make.bottom.equalTo(self).offset(-KB_TABBAR_HEIGHT - 40 - 10); + make.edges.equalTo(self); }]; + self.contentBottomInset = KB_TABBAR_HEIGHT + 40 + 10; + [self updateContentBottomInset:self.contentBottomInset]; + __weak typeof(self) weakSelf = self; self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{ __strong typeof(weakSelf) strongSelf = weakSelf; @@ -207,6 +209,14 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 [self updateFooterVisibility]; } +- (void)updateContentBottomInset:(CGFloat)bottomInset { + self.contentBottomInset = bottomInset; + UIEdgeInsets insets = self.tableView.contentInset; + insets.bottom = bottomInset; + self.tableView.contentInset = insets; + self.tableView.scrollIndicatorInsets = insets; +} + - (void)addMessage:(KBAiChatMessage *)message autoScroll:(BOOL)autoScroll { if (!message) { diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h index e9f3517..3016ad0 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.h @@ -26,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)appendAssistantMessage:(NSString *)text audioId:(nullable NSString *)audioId; +/// 更新聊天列表底部 inset +- (void)updateChatViewBottomInset:(CGFloat)bottomInset; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m index 4cc72a3..deb74c2 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m @@ -288,6 +288,10 @@ [self.chatView addMessage:message autoScroll:YES]; } +- (void)updateChatViewBottomInset:(CGFloat)bottomInset { + [self.chatView updateContentBottomInset:bottomInset]; +} + #pragma mark - KBChatTableViewDelegate - (void)chatTableViewDidScroll:(KBChatTableView *)chatView diff --git a/keyBoard/Class/AiTalk/V/KBVoiceInputBar.h b/keyBoard/Class/AiTalk/V/KBVoiceInputBar.h index d503a52..d5266f6 100644 --- a/keyBoard/Class/AiTalk/V/KBVoiceInputBar.h +++ b/keyBoard/Class/AiTalk/V/KBVoiceInputBar.h @@ -27,6 +27,18 @@ NS_ASSUME_NONNULL_BEGIN @end +typedef NS_ENUM(NSInteger, KBVoiceInputBarMode) { + KBVoiceInputBarModeText, + KBVoiceInputBarModeVoice +}; + +typedef NS_ENUM(NSInteger, KBVoiceInputBarState) { + KBVoiceInputBarStateText, + KBVoiceInputBarStateVoice, + KBVoiceInputBarStateRecording, + KBVoiceInputBarStateCancel +}; + /// 底部语音输入栏 /// 包含:毛玻璃背景 + 录音按钮 @interface KBVoiceInputBar : UIView @@ -37,6 +49,12 @@ NS_ASSUME_NONNULL_BEGIN /// 状态文本(显示在按钮上方) @property (nonatomic, copy) NSString *statusText; +/// 输入模式(文字/语音) +@property (nonatomic, assign) KBVoiceInputBarMode inputMode; + +/// 输入状态(文字/语音/录音/取消) +@property (nonatomic, assign) KBVoiceInputBarState inputState; + /// 是否启用(禁用时按钮不可点击) @property (nonatomic, assign) BOOL enabled; diff --git a/keyBoard/Class/AiTalk/V/KBVoiceInputBar.m b/keyBoard/Class/AiTalk/V/KBVoiceInputBar.m index 8da0d6d..3b96d3e 100644 --- a/keyBoard/Class/AiTalk/V/KBVoiceInputBar.m +++ b/keyBoard/Class/AiTalk/V/KBVoiceInputBar.m @@ -11,18 +11,37 @@ @interface KBVoiceInputBar () -/// 毛玻璃背景容器 -@property (nonatomic, strong) UIView *backgroundView; - -/// 毛玻璃效果 -@property (nonatomic, strong) UIVisualEffectView *blurEffectView; - /// 状态标签 @property (nonatomic, strong) UILabel *statusLabel; /// 录音按钮 @property (nonatomic, strong) KBAiRecordButton *recordButton; +/// 输入区域容器 +@property (nonatomic, strong) UIView *inputContainer; + +/// 文字输入视图 +@property (nonatomic, strong) UIView *textInputView; +@property (nonatomic, strong) UIButton *textCenterButton; + +/// 语音输入视图 +@property (nonatomic, strong) UIView *voiceInputView; +@property (nonatomic, strong) UILabel *voiceCenterLabel; + +/// 左侧切换按钮(麦克风/键盘共用) +@property (nonatomic, strong) UIButton *toggleIconButton; + +/// 录音中视图 +@property (nonatomic, strong) UIView *recordingView; +@property (nonatomic, strong) UIImageView *recordingCenterIconView; + +/// 取消视图 +@property (nonatomic, strong) UIView *cancelView; +@property (nonatomic, strong) UILabel *cancelLabel; + +/// 隐藏输入框(用于弹起键盘) +@property (nonatomic, strong) UITextField *hiddenTextField; + /// 是否正在录音 @property (nonatomic, assign) BOOL isRecording; @@ -52,30 +71,8 @@ self.backgroundColor = [UIColor clearColor]; self.enabled = YES; self.isRecording = NO; - - // 毛玻璃背景容器 - [self addSubview:self.backgroundView]; - [self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) { - make.edges.equalTo(self); - }]; - - // 毛玻璃效果 - [self.backgroundView addSubview:self.blurEffectView]; - [self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) { - make.edges.equalTo(self.backgroundView); - }]; - - // 为 blurEffectView 创建透明度渐变 mask - CAGradientLayer *maskLayer = [CAGradientLayer layer]; - maskLayer.startPoint = CGPointMake(0.5, 1); // 底部 - maskLayer.endPoint = CGPointMake(0.5, 0); // 顶部 - maskLayer.colors = @[ - (__bridge id)[UIColor whiteColor].CGColor, // 底部:完全不透明 - (__bridge id)[UIColor whiteColor].CGColor, // 中间:完全不透明 - (__bridge id)[UIColor clearColor].CGColor // 顶部:完全透明 - ]; - maskLayer.locations = @[@(0.0), @(0.5), @(1.0)]; - self.blurEffectView.layer.mask = maskLayer; + self.inputMode = KBVoiceInputBarModeVoice; + self.inputState = KBVoiceInputBarStateText; // 状态标签 [self addSubview:self.statusLabel]; @@ -86,24 +83,92 @@ make.height.mas_equalTo(20); }]; - // 录音按钮 - [self addSubview:self.recordButton]; - [self.recordButton mas_makeConstraints:^(MASConstraintMaker *make) { + // 输入区域容器 + [self addSubview:self.inputContainer]; + [self.inputContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.statusLabel.mas_bottom).offset(12); make.left.equalTo(self).offset(20); make.right.equalTo(self).offset(-20); make.height.mas_equalTo(50); make.bottom.lessThanOrEqualTo(self).offset(-16); }]; -} -- (void)layoutSubviews { - [super layoutSubviews]; + UILongPressGestureRecognizer *longPress = + [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(handleVoiceLongPress:)]; + longPress.minimumPressDuration = 0.05; + longPress.cancelsTouchesInView = NO; + [self.inputContainer addGestureRecognizer:longPress]; - // 更新 mask 的 frame - if (self.blurEffectView.layer.mask) { - self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds; - } + // 文字输入视图 + [self.inputContainer addSubview:self.textInputView]; + [self.textInputView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.inputContainer); + }]; + + [self.inputContainer addSubview:self.toggleIconButton]; + [self.toggleIconButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.textInputView).offset(16); + make.centerY.equalTo(self.textInputView); + make.width.height.mas_equalTo(24); + }]; + + [self.textInputView addSubview:self.textCenterButton]; + [self.textCenterButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.toggleIconButton.mas_right).offset(12); + make.right.equalTo(self.textInputView).offset(-16); + make.centerY.equalTo(self.textInputView); + make.height.mas_equalTo(30); + }]; + + // 语音输入视图 + [self.inputContainer addSubview:self.voiceInputView]; + [self.voiceInputView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.inputContainer); + }]; + + [self.voiceInputView addSubview:self.voiceCenterLabel]; + [self.voiceCenterLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.toggleIconButton.mas_right).offset(12); + make.right.equalTo(self.voiceInputView).offset(-16); + make.centerY.equalTo(self.voiceInputView); + }]; + + // 录音中视图 + [self.inputContainer addSubview:self.recordingView]; + [self.recordingView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.inputContainer); + }]; + + [self.recordingView addSubview:self.recordingCenterIconView]; + [self.recordingCenterIconView mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.recordingView); + make.width.height.mas_equalTo(36); + }]; + + // 取消视图 + [self.inputContainer addSubview:self.cancelView]; + [self.cancelView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.inputContainer); + }]; + + [self.cancelView addSubview:self.cancelLabel]; + [self.cancelLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.cancelView); + }]; + + // 隐藏输入框(仅用于弹起键盘) + [self addSubview:self.hiddenTextField]; + [self.hiddenTextField mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.height.mas_equalTo(1); + make.left.top.equalTo(self); + }]; + + // 隐藏旧的录音按钮和状态标签 + self.statusLabel.hidden = YES; + + // 统一设置初始状态 + self.inputState = KBVoiceInputBarStateText; } #pragma mark - Setter @@ -111,17 +176,55 @@ - (void)setStatusText:(NSString *)statusText { _statusText = [statusText copy]; self.statusLabel.text = statusText; + [self updateCenterTextIfNeeded]; } - (void)setEnabled:(BOOL)enabled { _enabled = enabled; self.recordButton.userInteractionEnabled = enabled; self.recordButton.alpha = enabled ? 1.0 : 0.5; + self.inputContainer.userInteractionEnabled = enabled; + self.inputContainer.alpha = enabled ? 1.0 : 0.5; } - (void)setRecording:(BOOL)recording { _isRecording = recording; self.recordButton.state = recording ? KBAiRecordButtonStateRecording : KBAiRecordButtonStateNormal; + if (recording) { + self.inputState = KBVoiceInputBarStateRecording; + } else if (self.inputState == KBVoiceInputBarStateRecording) { + self.inputState = KBVoiceInputBarStateVoice; + } +} + +- (void)setInputMode:(KBVoiceInputBarMode)inputMode { + _inputMode = inputMode; + if (inputMode == KBVoiceInputBarModeText) { + [self.toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"] + forState:UIControlStateNormal]; + } else { + [self.toggleIconButton setImage:[UIImage imageNamed:@"ai_jianpan_icon"] + forState:UIControlStateNormal]; + } +} + +- (void)setInputState:(KBVoiceInputBarState)inputState { + _inputState = inputState; + self.textInputView.hidden = (inputState != KBVoiceInputBarStateText); + self.voiceInputView.hidden = (inputState != KBVoiceInputBarStateVoice); + self.recordingView.hidden = (inputState != KBVoiceInputBarStateRecording); + self.cancelView.hidden = (inputState != KBVoiceInputBarStateCancel); + self.toggleIconButton.hidden = (inputState == KBVoiceInputBarStateRecording || + inputState == KBVoiceInputBarStateCancel); + if (inputState == KBVoiceInputBarStateText) { + self.inputMode = KBVoiceInputBarModeText; + } else if (inputState == KBVoiceInputBarStateVoice) { + self.inputMode = KBVoiceInputBarModeVoice; + } + if (!self.toggleIconButton.hidden) { + [self.inputContainer bringSubviewToFront:self.toggleIconButton]; + } + [self updateCenterTextIfNeeded]; } #pragma mark - Public Methods @@ -162,22 +265,6 @@ #pragma mark - Lazy Load -- (UIView *)backgroundView { - if (!_backgroundView) { - _backgroundView = [[UIView alloc] init]; - _backgroundView.clipsToBounds = YES; - } - return _backgroundView; -} - -- (UIVisualEffectView *)blurEffectView { - if (!_blurEffectView) { - UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; - _blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; - } - return _blurEffectView; -} - - (UILabel *)statusLabel { if (!_statusLabel) { _statusLabel = [[UILabel alloc] init]; @@ -195,8 +282,198 @@ _recordButton.delegate = self; _recordButton.normalTitle = @"按住说话"; _recordButton.recordingTitle = @"松开结束"; + _recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"]; + _recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"]; + _recordButton.hidden = YES; } return _recordButton; } +- (UIView *)inputContainer { + if (!_inputContainer) { + _inputContainer = [[UIView alloc] init]; + _inputContainer.clipsToBounds = YES; + _inputContainer.layer.cornerRadius = 25; + _inputContainer.backgroundColor = [UIColor clearColor]; + } + return _inputContainer; +} + +- (UIView *)textInputView { + if (!_textInputView) { + _textInputView = [[UIView alloc] init]; + _textInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7]; + } + return _textInputView; +} + +- (UIButton *)textCenterButton { + if (!_textCenterButton) { + _textCenterButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal]; + _textCenterButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; + [_textCenterButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + _textCenterButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; + [_textCenterButton addTarget:self + action:@selector(handleTextCenterTap) + forControlEvents:UIControlEventTouchUpInside]; + } + return _textCenterButton; +} + +- (UIView *)voiceInputView { + if (!_voiceInputView) { + _voiceInputView = [[UIView alloc] init]; + _voiceInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7]; + } + return _voiceInputView; +} + +- (UILabel *)voiceCenterLabel { + if (!_voiceCenterLabel) { + _voiceCenterLabel = [[UILabel alloc] init]; + _voiceCenterLabel.text = @"按住说话"; + _voiceCenterLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; + _voiceCenterLabel.textColor = [UIColor whiteColor]; + _voiceCenterLabel.textAlignment = NSTextAlignmentCenter; + } + return _voiceCenterLabel; +} + +- (UIButton *)toggleIconButton { + if (!_toggleIconButton) { + _toggleIconButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"] + forState:UIControlStateNormal]; + [_toggleIconButton addTarget:self + action:@selector(handleToggleIconTap) + forControlEvents:UIControlEventTouchUpInside]; + _toggleIconButton.exclusiveTouch = YES; + } + return _toggleIconButton; +} + +- (UIView *)recordingView { + if (!_recordingView) { + _recordingView = [[UIView alloc] init]; + _recordingView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6]; + } + return _recordingView; +} + +- (UIImageView *)recordingCenterIconView { + if (!_recordingCenterIconView) { + _recordingCenterIconView = [[UIImageView alloc] init]; + _recordingCenterIconView.image = [UIImage imageNamed:@"ai_luyining_icon"]; + _recordingCenterIconView.contentMode = UIViewContentModeScaleAspectFit; + } + return _recordingCenterIconView; +} + +- (UIView *)cancelView { + if (!_cancelView) { + _cancelView = [[UIView alloc] init]; + _cancelView.backgroundColor = [UIColor colorWithRed:0.75 green:0.3 blue:0.3 alpha:1.0]; + } + return _cancelView; +} + +- (UILabel *)cancelLabel { + if (!_cancelLabel) { + _cancelLabel = [[UILabel alloc] init]; + _cancelLabel.text = @"Release To Cancel"; + _cancelLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; + _cancelLabel.textColor = [UIColor whiteColor]; + } + return _cancelLabel; +} + +- (UITextField *)hiddenTextField { + if (!_hiddenTextField) { + _hiddenTextField = [[UITextField alloc] init]; + _hiddenTextField.hidden = YES; + } + return _hiddenTextField; +} + +#pragma mark - Actions + +- (void)handleToggleIconTap { + if (self.inputState == KBVoiceInputBarStateText) { + self.inputState = KBVoiceInputBarStateVoice; + [self endEditing:YES]; + } else { + self.inputState = KBVoiceInputBarStateText; + } +} + +- (void)handleTextCenterTap { + self.inputState = KBVoiceInputBarStateText; + [self.hiddenTextField becomeFirstResponder]; +} + +- (void)handleVoiceLongPress:(UILongPressGestureRecognizer *)gesture { + if (self.inputState != KBVoiceInputBarStateVoice && + self.inputState != KBVoiceInputBarStateRecording && + self.inputState != KBVoiceInputBarStateCancel) { + return; + } + + CGPoint location = [gesture locationInView:self.inputContainer]; + BOOL isInside = CGRectContainsPoint(self.inputContainer.bounds, location); + CGPoint iconPoint = [gesture locationInView:self.toggleIconButton]; + BOOL isOnToggleIcon = CGRectContainsPoint(self.toggleIconButton.bounds, iconPoint); + if (isOnToggleIcon) { + return; + } + + switch (gesture.state) { + case UIGestureRecognizerStateBegan: { + if (self.inputState != KBVoiceInputBarStateVoice) { + return; + } + self.inputState = KBVoiceInputBarStateRecording; + if ([self.delegate respondsToSelector:@selector(voiceInputBarDidBeginRecording:)]) { + [self.delegate voiceInputBarDidBeginRecording:self]; + } + } break; + case UIGestureRecognizerStateChanged: { + if (isInside) { + self.inputState = KBVoiceInputBarStateRecording; + } else { + self.inputState = KBVoiceInputBarStateCancel; + } + } break; + case UIGestureRecognizerStateEnded: { + if (isInside) { + if ([self.delegate respondsToSelector:@selector(voiceInputBarDidEndRecording:)]) { + [self.delegate voiceInputBarDidEndRecording:self]; + } + } else { + if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) { + [self.delegate voiceInputBarDidCancelRecording:self]; + } + } + self.inputState = KBVoiceInputBarStateVoice; + } break; + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: { + if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) { + [self.delegate voiceInputBarDidCancelRecording:self]; + } + self.inputState = KBVoiceInputBarStateVoice; + } break; + default: + break; + } +} + +- (void)updateCenterTextIfNeeded { + if (self.inputState == KBVoiceInputBarStateText) { + [self.textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal]; + } else if (self.inputState == KBVoiceInputBarStateVoice) { + self.voiceCenterLabel.text = @"按住说话"; + } +} + @end diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index a19132a..01dfc70 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -13,15 +13,30 @@ #import "KBVoiceToTextManager.h" #import "AiVM.h" #import "KBHUD.h" +#import "KBChatLimitPopView.h" +#import "KBVipPay.h" +#import "KBUserSessionManager.h" +#import "LSTPopView.h" #import -@interface KBAIHomeVC () +@interface KBAIHomeVC () /// 人设列表容器 @property (nonatomic, strong) UICollectionView *collectionView; /// 底部语音输入栏 @property (nonatomic, strong) KBVoiceInputBar *voiceInputBar; +@property (nonatomic, strong) MASConstraint *voiceInputBarBottomConstraint; +@property (nonatomic, assign) CGFloat voiceInputBarHeight; +@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing; +@property (nonatomic, assign) CGFloat currentKeyboardHeight; +@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap; +@property (nonatomic, weak) LSTPopView *chatLimitPopView; + +/// 底部毛玻璃背景 +@property (nonatomic, strong) UIView *bottomBackgroundView; +@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView; +@property (nonatomic, strong) CAGradientLayer *bottomMaskLayer; /// 语音转写管理器 @property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager; @@ -72,23 +87,47 @@ [self setupUI]; [self setupVoiceToTextManager]; [self setupVoiceRecordManager]; + [self setupKeyboardNotifications]; + [self setupKeyboardDismissGesture]; [self loadPersonas]; } +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + if (self.bottomMaskLayer) { + self.bottomMaskLayer.frame = self.bottomBlurEffectView.bounds; + } +} + #pragma mark - 1:控件初始化 - (void)setupUI { + self.voiceInputBarHeight = 150.0; + self.baseInputBarBottomSpacing = 20.0; [self.view addSubview:self.collectionView]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; + // 底部毛玻璃背景 + [self.view addSubview:self.bottomBackgroundView]; + [self.bottomBackgroundView addSubview:self.bottomBlurEffectView]; + [self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self.view); +// self.bottomBackgroundBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing); + make.bottom.equalTo(self.view); + make.height.mas_equalTo(self.voiceInputBarHeight); + }]; + [self.bottomBlurEffectView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.bottomBackgroundView); + }]; + // 底部语音输入栏 [self.view addSubview:self.voiceInputBar]; [self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); - make.bottom.equalTo(self.view).offset(-20); - make.height.mas_equalTo(150); // 根据实际需要调整高度 + self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing); + make.height.mas_equalTo(self.voiceInputBarHeight); // 根据实际需要调整高度 }]; } @@ -206,6 +245,7 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath]; cell.persona = self.personas[indexPath.item]; + [self updateChatViewBottomInset]; // 标记为已预加载 [self.preloadedIndexes addObject:@(indexPath.item)]; @@ -244,6 +284,8 @@ if (currentPage < self.personas.count) { NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name); } + + [self updateChatViewBottomInset]; } #pragma mark - 4:语音转写 @@ -262,6 +304,61 @@ self.voiceRecordManager.minRecordDuration = 1.0; } +#pragma mark - 6:键盘监听 + +- (void)setupKeyboardNotifications { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleKeyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; +} + +- (void)handleKeyboardWillChangeFrame:(NSNotification *)notification { + NSDictionary *userInfo = notification.userInfo; + CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16); + + CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil]; + CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame)); + self.currentKeyboardHeight = keyboardHeight; + + CGFloat bottomSpacing = (keyboardHeight > 0.0) ? (keyboardHeight + 8.0) : self.baseInputBarBottomSpacing; + [self.voiceInputBarBottomConstraint setOffset:-bottomSpacing]; + [self updateChatViewBottomInset]; + + [UIView animateWithDuration:duration + delay:0 + options:options + animations:^{ + [self.view layoutIfNeeded]; + } + completion:nil]; +} + +#pragma mark - 7:键盘收起 + +- (void)setupKeyboardDismissGesture { + self.dismissKeyboardTap = [[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(handleBackgroundTap)]; + self.dismissKeyboardTap.cancelsTouchesInView = NO; + self.dismissKeyboardTap.delegate = self; + [self.view addGestureRecognizer:self.dismissKeyboardTap]; +} + +- (void)handleBackgroundTap { + [self.view endEditing:YES]; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + if ([touch.view isDescendantOfView:self.voiceInputBar]) { + return NO; + } + return YES; +} + - (NSInteger)currentCompanionId { if (self.personas.count == 0) { return 0; @@ -302,6 +399,42 @@ return nil; } +#pragma mark - Private + +- (void)updateChatViewBottomInset { + CGFloat bottomSpacing = (self.currentKeyboardHeight > 0.0) ? (self.currentKeyboardHeight + 8.0) : self.baseInputBarBottomSpacing; + CGFloat bottomInset = self.voiceInputBarHeight + bottomSpacing; + + for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) { + KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + if (cell) { + [cell updateChatViewBottomInset:bottomInset]; + } + } +} + +- (void)showChatLimitPopWithMessage:(NSString *)message { + if (self.chatLimitPopView) { + [self.chatLimitPopView dismiss]; + } + + CGFloat width = KB_SCREEN_WIDTH - 60; + KBChatLimitPopView *content = [[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)]; + content.message = message; + content.delegate = self; + + LSTPopView *pop = [LSTPopView initWithCustomView:content + parentView:nil + popStyle:LSTPopStyleFade + dismissStyle:LSTDismissStyleFade]; + pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + pop.hemStyle = LSTHemStyleCenter; + pop.isClickBgDismiss = YES; + pop.isAvoidKeyboard = NO; + self.chatLimitPopView = pop; + [pop pop]; +} + #pragma mark - Lazy Load - (UICollectionView *)collectionView { @@ -335,59 +468,59 @@ return _voiceInputBar; } +#pragma mark - KBChatLimitPopViewDelegate + +- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view { + [self.chatLimitPopView dismiss]; +} + +- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view { + [self.chatLimitPopView dismiss]; + if (![KBUserSessionManager shared].isLoggedIn) { + [[KBUserSessionManager shared] goLoginVC]; + return; + } + KBVipPay *vc = [[KBVipPay alloc] init]; + [KB_CURRENT_NAV pushViewController:vc animated:true]; +} + +- (UIView *)bottomBackgroundView { + if (!_bottomBackgroundView) { + _bottomBackgroundView = [[UIView alloc] init]; + _bottomBackgroundView.clipsToBounds = YES; + } + return _bottomBackgroundView; +} + +- (UIVisualEffectView *)bottomBlurEffectView { + if (!_bottomBlurEffectView) { + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + _bottomBlurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + _bottomBlurEffectView.layer.mask = self.bottomMaskLayer; + } + return _bottomBlurEffectView; +} + +- (CAGradientLayer *)bottomMaskLayer { + if (!_bottomMaskLayer) { + _bottomMaskLayer = [CAGradientLayer layer]; + _bottomMaskLayer.startPoint = CGPointMake(0.5, 1); + _bottomMaskLayer.endPoint = CGPointMake(0.5, 0); + _bottomMaskLayer.colors = @[ + (__bridge id)[UIColor whiteColor].CGColor, + (__bridge id)[UIColor whiteColor].CGColor, + (__bridge id)[UIColor clearColor].CGColor + ]; + _bottomMaskLayer.locations = @[@(0.0), @(0.5), @(1.0)]; + } + return _bottomMaskLayer; +} + #pragma mark - KBVoiceToTextManagerDelegate - (void)voiceToTextManager:(KBVoiceToTextManager *)manager didReceiveFinalText:(NSString *)text { - if (text.length == 0) { - return; - } - NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text); - - NSInteger companionId = [self currentCompanionId]; - if (companionId <= 0) { - NSLog(@"[KBAIHomeVC] companionId 无效,取消请求"); - return; - } - - KBPersonaChatCell *currentCell = [self currentPersonaCell]; - if (currentCell) { - [currentCell appendUserMessage:text]; - } - - __weak typeof(self) weakSelf = self; - [self.aiVM requestChatMessageWithContent:text - companionId:companionId - completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - if (error) { - NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); - return; - } - - if (!response || !response.data) { - NSLog(@"[KBAIHomeVC] 聊天响应为空"); - return; - } - - NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @""; - NSString *audioId = response.data.audioId; - if (aiResponse.length == 0) { - NSLog(@"[KBAIHomeVC] AI 回复为空"); - return; - } - - KBPersonaChatCell *cell = [strongSelf currentPersonaCell]; - if (cell) { - [cell appendAssistantMessage:aiResponse audioId:audioId]; - } - }); - }]; + [self handleTranscribedText:text]; } - (void)voiceToTextManager:(KBVoiceToTextManager *)manager @@ -417,6 +550,32 @@ error:nil]; unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue]; NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize); + + __weak typeof(self) weakSelf = self; + [self.aiVM transcribeAudioFileAtURL:fileURL + completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription); + [KBHUD showError:KBLocalized(@"语音转文字失败,请重试")]; + return; + } + + NSString *transcript = response.data.transcript ?: @""; + if (transcript.length == 0) { + NSLog(@"[KBAIHomeVC] 语音转文字结果为空"); + [KBHUD showError:KBLocalized(@"未识别到语音内容")]; + return; + } + + [strongSelf handleTranscribedText:transcript]; + }); + }]; } - (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager { @@ -429,4 +588,72 @@ NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription); } +#pragma mark - Private + +- (void)handleTranscribedText:(NSString *)text { + if (text.length == 0) { + return; + } + NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text); + + NSInteger companionId = [self currentCompanionId]; + if (companionId <= 0) { + NSLog(@"[KBAIHomeVC] companionId 无效,取消请求"); + return; + } + + KBPersonaChatCell *currentCell = [self currentPersonaCell]; + if (currentCell) { + [currentCell appendUserMessage:text]; + } + + __weak typeof(self) weakSelf = self; + [self.aiVM requestChatMessageWithContent:text + companionId:companionId + completion:^(KBAiMessageResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ +// if (error) { +// NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); +// return; +// } + + if (response.code == 50030) { + NSString *message = response.message ?: @""; + [strongSelf showChatLimitPopWithMessage:message]; + return; + } + + if (!response || !response.data) { + NSString *message = response.message ?: @"聊天响应为空"; + NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message); + if (message.length > 0) { + [KBHUD showError:message]; + } + return; + } + + NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @""; + NSString *audioId = response.data.audioId; + if (aiResponse.length == 0) { + NSLog(@"[KBAIHomeVC] AI 回复为空"); + return; + } + + KBPersonaChatCell *cell = [strongSelf currentPersonaCell]; + if (cell) { + [cell appendAssistantMessage:aiResponse audioId:audioId]; + } + }); + }]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + @end diff --git a/keyBoard/Class/AiTalk/VC/KBAiMainVC.m b/keyBoard/Class/AiTalk/VC/KBAiMainVC.m index 67839ab..c126991 100644 --- a/keyBoard/Class/AiTalk/VC/KBAiMainVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAiMainVC.m @@ -14,6 +14,8 @@ #import "KBChatTableView.h" #import "KBAiRecordButton.h" #import "KBHUD.h" +#import "KBChatLimitPopView.h" +#import "KBVipPay.h" #import "LSTPopView.h" #import "VoiceChatStreamingManager.h" #import "KBUserSessionManager.h" @@ -22,8 +24,10 @@ @interface KBAiMainVC () + AVAudioPlayerDelegate, + KBChatLimitPopViewDelegate> @property(nonatomic, weak) LSTPopView *popView; +@property(nonatomic, weak) LSTPopView *limitPopView; // UI @property(nonatomic, strong) KBChatTableView *chatView; @@ -419,6 +423,48 @@ self.commentView = customView; } +#pragma mark - 次数用尽弹窗 + +- (void)showChatLimitPopWithMessage:(NSString *)message { + if (self.limitPopView) { + [self.limitPopView dismiss]; + } + + CGFloat width = KB_SCREEN_WIDTH - 60; + KBChatLimitPopView *content = + [[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, 180)]; + content.message = message; + content.delegate = self; + + LSTPopView *popView = + [LSTPopView initWithCustomView:content + parentView:nil + popStyle:LSTPopStyleFade + dismissStyle:LSTDismissStyleFade]; + popView.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + popView.hemStyle = LSTHemStyleCenter; + popView.isClickBgDismiss = YES; + popView.isAvoidKeyboard = NO; + self.limitPopView = popView; + [popView pop]; +} + +#pragma mark - KBChatLimitPopViewDelegate + +- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view { + [self.limitPopView dismiss]; +} + +- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view { + [self.limitPopView dismiss]; + if (![KBUserSessionManager shared].isLoggedIn) { + [[KBUserSessionManager shared] goLoginVC]; + return; + } + KBVipPay *vc = [[KBVipPay alloc] init]; + [KB_CURRENT_NAV pushViewController:vc animated:true]; +} + #pragma mark - UI Updates - (void)updateStatusForState:(ConversationState)state { @@ -685,6 +731,18 @@ return; } + if (response.code == 50030) { + NSString *message = response.message ?: @""; + [strongSelf showChatLimitPopWithMessage:message]; + return; + } + + if (!response || !response.data) { + NSString *message = response.message ?: @"AI 回复为空"; + [KBHUD showError:message]; + return; + } + // 获取 AI 回复文本 NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @""; diff --git a/keyBoard/Class/AiTalk/VM/AiVM.h b/keyBoard/Class/AiTalk/VM/AiVM.h index af09f21..bb18cf7 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.h +++ b/keyBoard/Class/AiTalk/VM/AiVM.h @@ -37,6 +37,7 @@ typedef void (^AiVMSyncCompletion)(KBAiSyncResponse *_Nullable response, @interface KBAiMessageResponse : NSObject @property(nonatomic, assign) NSInteger code; @property(nonatomic, strong, nullable) KBAiMessageData *data; +@property(nonatomic, copy, nullable) NSString *message; @end typedef void (^AiVMMessageCompletion)(KBAiMessageResponse *_Nullable response, @@ -47,6 +48,22 @@ typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL, typedef void (^AiVMUploadAudioCompletion)(NSString *_Nullable fileURL, NSError *_Nullable error); +@interface KBAiSpeechTranscribeData : NSObject +@property(nonatomic, copy, nullable) NSString *transcript; +@property(nonatomic, assign) double confidence; +@property(nonatomic, assign) double duration; +@property(nonatomic, copy, nullable) NSString *detectedLanguage; +@end + +@interface KBAiSpeechTranscribeResponse : NSObject +@property(nonatomic, assign) NSInteger code; +@property(nonatomic, strong, nullable) KBAiSpeechTranscribeData *data; +@property(nonatomic, copy, nullable) NSString *message; +@end + +typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nullable response, + NSError *_Nullable error); + @interface AiVM : NSObject - (void)syncChatWithTranscript:(NSString *)transcript @@ -64,6 +81,10 @@ typedef void (^AiVMUploadAudioCompletion)(NSString *_Nullable fileURL, - (void)uploadAudioFileAtURL:(NSURL *)fileURL completion:(AiVMUploadAudioCompletion)completion; +/// 语音转文字(multipart/form-data) +- (void)transcribeAudioFileAtURL:(NSURL *)fileURL + completion:(AiVMSpeechTranscribeCompletion)completion; + #pragma mark - 人设相关接口 /// 分页查询人设列表 diff --git a/keyBoard/Class/AiTalk/VM/AiVM.m b/keyBoard/Class/AiTalk/VM/AiVM.m index 510237f..ff42f71 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.m +++ b/keyBoard/Class/AiTalk/VM/AiVM.m @@ -46,6 +46,12 @@ @implementation KBAiMessageResponse @end +@implementation KBAiSpeechTranscribeData +@end + +@implementation KBAiSpeechTranscribeResponse +@end + @implementation AiVM - (void)syncChatWithTranscript:(NSString *)transcript @@ -126,15 +132,16 @@ autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { + KBAiMessageResponse *model = + [KBAiMessageResponse mj_objectWithKeyValues:json]; if (error) { if (completion) { - completion(nil, error); + completion(model, error); } return; } - KBAiMessageResponse *model = - [KBAiMessageResponse mj_objectWithKeyValues:json]; + id dataObj = json[@"data"]; if (!model.data && [dataObj isKindOfClass:[NSString class]]) { KBAiMessageData *data = [[KBAiMessageData alloc] init]; @@ -261,6 +268,42 @@ autoShowBusinessError:NO }]; } +- (void)transcribeAudioFileAtURL:(NSURL *)fileURL + completion:(AiVMSpeechTranscribeCompletion)completion { + if (!fileURL || !fileURL.isFileURL) { + NSError *error = [NSError errorWithDomain:@"AiVM" + code:-1 + userInfo:@{NSLocalizedDescriptionKey : @"invalid fileURL"}]; + if (completion) { + completion(nil, error); + } + return; + } + + [[KBNetworkManager shared] uploadFile:API_AI_SPEECH_TRANSCRIBE + fileURL:fileURL + name:@"file" + mimeType:@"audio/m4a" + parameters:nil + headers:nil + completion:^(NSDictionary *_Nullable json, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + if (completion) { + completion(nil, error); + } + return; + } + + KBAiSpeechTranscribeResponse *model = + [KBAiSpeechTranscribeResponse mj_objectWithKeyValues:json]; + if (completion) { + completion(model, nil); + } + }]; +} + #pragma mark - 人设相关接口 - (void)fetchPersonasWithPageNum:(NSInteger)pageNum