Browse Source

ai 回答问题,逐字输出

100Years 1 month ago
parent
commit
57376e8ef1

+ 4 - 12
AIEmoji/AppDelegate.swift

@@ -17,10 +17,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         window = UIWindow(frame: UIScreen.main.bounds)
         window?.backgroundColor = UIColor.white
         window?.makeKeyAndVisible()
-        
-        // 监听应用进入后台的通知
-        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
-        
         initPlatform()
         goToLoadVC()
         return true
@@ -53,8 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         }
     }
     
-    
-    
+
     func initPlatform() {
         TSColorConfigShared.naviMianTextColor = .white
     }
@@ -62,16 +57,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 }
 
 extension AppDelegate {
-    
+
     func applicationWillTerminate(_ application: UIApplication) {
         // 当应用即将被终止时,这里也可以添加数据保存逻辑,但系统留给的时间很有限
         NotificationCenter.default.post(name: .kApplicationWillTerminate, object: nil)
     }
-}
-
-
-extension AppDelegate {
-    @objc func applicationDidEnterBackground() {
+    
+    func applicationDidEnterBackground(_ application: UIApplication) {
         beginBackgroundTask()
     }
     

+ 0 - 23
AIEmoji/Business/AIChat/TSChatViewController/Layout/CustomMessageFlowLayout.swift

@@ -41,30 +41,7 @@ open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout {
 
   open override func messageSizeCalculators() -> [MessageSizeCalculator] {
     var superCalculators = super.messageSizeCalculators()
-    // Append any of your custom `MessageSizeCalculator` if you wish for the convenience
-    // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...`
     superCalculators.append(customMessageSizeCalculator)
     return superCalculators
   }
 }
-
-// MARK: - CustomMessageSizeCalculator
-
-open class CustomMessageSizeCalculator: MessageSizeCalculator {
-  // MARK: Lifecycle
-
-  public override init(layout: MessagesCollectionViewFlowLayout? = nil) {
-    super.init()
-    self.layout = layout
-  }
-
-  // MARK: Open
-
-  open override func sizeForItem(at _: IndexPath) -> CGSize {
-    guard let layout = layout else { return .zero }
-    let collectionViewWidth = layout.collectionView?.bounds.width ?? 0
-    let contentInset = layout.collectionView?.contentInset ?? .zero
-    let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right
-    return CGSize(width: collectionViewWidth - inset, height: 44)
-  }
-}

+ 8 - 2
AIEmoji/Business/AIChat/TSChatViewController/Models/TSTextLayoutSizeCalculator.swift

@@ -27,6 +27,12 @@ class TSTextLayoutSizeCalculator: TSLayoutSizeCalculator {
         messageContainerTraingMaxWidth - Self.cellMessagelabelEdge.left - Self.cellMessagelabelEdge.right
     }
     
+
+    func cellMessagelabelMaxWidth(fromCurrentSender: Bool) -> CGFloat {
+        let maxWidth = fromCurrentSender ? cellMessagelabelTraingMaxWidth: cellMessagelabelLeadingMaxWidth
+        return maxWidth
+    }
+    
     //消息容器的高度
     override func cellContentHeight(for message: MessageType,at indexPath: IndexPath, fromCurrentSender: Bool)-> CGFloat {
         let superH = super.cellContentHeight(for: message, at: indexPath, fromCurrentSender: fromCurrentSender)
@@ -54,12 +60,12 @@ class TSTextLayoutSizeCalculator: TSLayoutSizeCalculator {
         default:
             fatalError("messageLabelSize received unhandled MessageDataType: \(message.kind)")
         }
-        let maxWidth = fromCurrentSender ? cellMessagelabelTraingMaxWidth: cellMessagelabelLeadingMaxWidth
+        let maxWidth = cellMessagelabelMaxWidth(fromCurrentSender: fromCurrentSender)
 //        let textSize = sendText.height(ofFont: Self.messageLabelFont, maxWidth: maxWidth)
 
         //用 label 计算更加精准
         var size = UILabel.getAttributedTextSize(attributedText: attributedText, maxWidth: maxWidth)
-
+//        debugPrint("attributedText size=\(size)")
 //        var size = attributedText.size(consideringWidth: maxWidth)
 //        size.width = size.width + 10
 

+ 4 - 18
AIEmoji/Business/AIChat/TSChatViewController/TSChatViewController/TSChatViewController+Keyboard.swift

@@ -45,30 +45,14 @@ extension TSChatViewController{
         let frameHeight = scrollView.frame.size.height
         
         // 判断是否需要显示滚动到底部的按钮
-        if offsetY > contentHeight - frameHeight + inputContainerView.frame.size.height - 10 {
+        if offsetY > contentHeight - frameHeight + inputContainerView.frame.size.height - 40 {
             scrollToBottomButton.isHidden = true
-            updateMessageCollectionViewBottomInset()
         } else {
             scrollToBottomButton.isHidden = false
         }
         
       
     }
-    
-    /// Updates bottom messagesCollectionView inset based on the position of inputContainerView
-    func updateMessageCollectionViewBottomInset() {
-      let collectionViewHeight = messagesCollectionView.frame.maxY
-      let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) -
-        (messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom)
-      let normalizedNewBottomInset = max(0, newBottomInset)
-      let differenceOfBottomInset = newBottomInset - messagesCollectionView.contentInset.bottom
-
-      UIView.performWithoutAnimation {
-        guard differenceOfBottomInset != 0 else { return }
-        messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
-        messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
-      }
-    }
 }
 
 
@@ -154,7 +138,9 @@ extension TSChatViewController{
             return
         }
 
-        let contentInset = UIEdgeInsets.zero
+        let differenceOfBottomInset = inputContainerView.bounds.size.height
+        let contentInset = UIEdgeInsets(top: 0, left: 0, bottom: differenceOfBottomInset, right: 0)
+        
         UIView.animate(withDuration: animationDuration) {
             self.messagesCollectionView.contentInset = contentInset
             self.messagesCollectionView.scrollIndicatorInsets = contentInset

+ 88 - 40
AIEmoji/Business/AIChat/TSChatViewController/TSChatViewController/TSChatViewController+SendMsg.swift

@@ -17,7 +17,7 @@ extension TSChatViewController {
         inputBarVC.emptyInput()
         
         sendMessages(data)
-        messagesCollectionView.scrollToLastItem(animated: true)
+        scrollToBottom()
         view.endEditing(true)
     }
     
@@ -48,37 +48,44 @@ extension TSChatViewController {
             return
         }
         
+        //先插入回答的消息,转圈加载
         let message = TSChatMessage(attributedText: kMDAttributedString(text: ""), user: viewModel.kAIUser, messageId: UUID().uuidString, date: Date())
         message.sendState = .start
         insertMessage(message)
-        
-        inputBarVC.sendEnabled(enabled: false)
-
         NotificationCenter.default.post(name: .kAIAnsweringNotification, object: nil, userInfo: [kIsAIAnswering: true])
+
+//        //每次全部输出
+//        viewModel.sendChatMessage(message: messageString) {[weak self] string in
+//            guard let self = self else { return }
+//            debugPrint("viewModel.AiMDString=\(viewModel.AiMDString)")
+//            message.kind = .attributedText(kMDAttributedString(text: viewModel.AiMDString))
+//            message.sendState = .progress(0.5)
+//
+//            if self.scrollToBottomButton.isHidden == true {
+//                updataAIChatCellUI()
+//                self.messagesCollectionView.scrollToLastItem(animated: false)
+//            }
+//        }
         
+        //逐字输出
+        var previousAiMDString = ""
         viewModel.sendChatMessage(message: messageString) {[weak self] string in
             guard let self = self else { return }
             debugPrint("viewModel.AiMDString=\(viewModel.AiMDString)")
-            message.kind = .attributedText(kMDAttributedString(text: viewModel.AiMDString))
             message.sendState = .progress(0.5)
-           
-            if self.scrollToBottomButton.isHidden == true {
-                updataAIChatCellUI()
-                self.messagesCollectionView.scrollToLastItem(animated: false)
-            }
-            
-        } completion: {[weak self] data, error in
+            delayedOutputAnimation(message: message, previousStr: previousAiMDString, newAddStr: string)
+            previousAiMDString = viewModel.AiMDString
+        }
+        
+        completion: {[weak self] data, error in
             guard let self = self else { return }
-            if let netData = data {
+            if let _ = data {
                 message.sendState = .success("netData")
-                //保存这条消息到本地数据库
-                //消耗一次 AI 次数
-                kPurchaseDefault.useOnceForFree(type: .aichat)
-                
+                kPurchaseDefault.useOnceForFree(type: .aichat)//消耗一次 AI 次数
+                message.kind = .attributedText(kMDAttributedString(text: viewModel.AiMDString))
             }else {
                 message.kind = .attributedText(kMDAttributedString(text: kAIErrorString))
                 message.sendState = .failed(kAIErrorString)
-                //保存这条消息到本地数据库
             }
             updataAIChatCellUI()
             
@@ -91,52 +98,93 @@ extension TSChatViewController {
                 }
                 
                 if self.scrollToBottomButton.isHidden == true {
-                    self.messagesCollectionView.scrollToLastItem(animated: false)
+                    self.scrollToBottom()
                 }
             }
         }
     }
     
     func updataAIChatCellUI(){
-//        if self.scrollToBottomButton.isHidden == true {
-//        if isLastItemVisible() {
-            kExecuteOnMainThread {
-                if self.messageList.count >= 2 {
-                    UIView.performWithoutAnimation {
-                        self.messagesCollectionView.reloadItems(at: [self.lastIndexPath])
-                    }
-                }else{
-                    self.messagesCollectionView.reloadData()
+        kExecuteOnMainThread {
+            if self.messageList.count >= 2 {
+                UIView.performWithoutAnimation {
+                    self.messagesCollectionView.reloadItems(at: [self.lastIndexPath])
                 }
+            }else{
+                self.messagesCollectionView.reloadData()
             }
-//        }
+        }
     }
     
-    
-    
     // 判断是否显示最后一个单元格
     func isLastItemVisible() -> Bool {
-        guard let indexPath = getIndexPathForLastItem() else { return false }
         let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems
         return visibleIndexPaths.contains(lastIndexPath)
     }
 
+    func scrollToBottom() {
+        self.messagesCollectionView.scrollToLastItem(animated: false)
+    }
+}
+
 
-    // 获取最后一个单元格的 indexPath
-    func getIndexPathForLastItem() -> IndexPath? {
-        let section = messagesCollectionView.numberOfSections - 1
-        if messagesCollectionView.numberOfItems(inSection: section) > 0 {
-            let item = messagesCollectionView.numberOfItems(inSection: section) - 1
-            return IndexPath(item: item, section: section)
+extension  TSChatViewController{
+    
+    func delayedOutputAnimation(message:TSChatMessage,previousStr:String,newAddStr:String, delay: TimeInterval = 0.05)  {
+        var showUIString = previousStr
+        let characters = Array(newAddStr)// 将 newText 转换为字符数组
+        
+        // 使用递归逐字显示
+        func typeCharacter(at index: Int) {
+            guard index < characters.count else { return } // 递归终止条件
+            showUIString.append(String(characters[index]))
+            // 设置延迟,逐字显示
+            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+                if self.scrollToBottomButton.isHidden == true {
+                    message.kind = .attributedText(kMDAttributedString(text: showUIString))
+                    self.updataAIChatCellUI()
+                    self.scrollToBottom()
+                }
+                typeCharacter(at: index + 1) // 递归调用
+            }
         }
-        return nil
+        
+        // 开始逐字显示
+        typeCharacter(at: 0)
     }
+    
+    
+    func typeTextAnimation(label: UILabel, originalText: String, newText: String, delay: TimeInterval = 0.05) {
+        // 确保 label 当前显示的文本是 originalText
+        label.text = originalText
+
+        // 将 newText 转换为字符数组
+        let characters = Array(newText)
 
+        // 使用递归逐字显示
+        func typeCharacter(at index: Int) {
+            guard index < characters.count else { return } // 递归终止条件
+
+            // 更新 label 的文本
+            let text =  String(characters[0...index])
+            label.text = text
+            label.attributedText = kMDAttributedString(text: text)
+            
+            // 设置延迟,逐字显示
+            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+                typeCharacter(at: index + 1) // 递归调用
+            }
+        }
+
+        // 开始逐字显示
+        typeCharacter(at: originalText.count)
+    }
 }
 
+let kIsAIAnswering = "isAIAnswering"
 public extension Notification.Name {
     //AI 回答中通知
     static let kAIAnsweringNotification = Self.init("kAIAnsweringNotification")
 }
-let kIsAIAnswering = "isAIAnswering"
+
 

+ 6 - 4
AIEmoji/Business/AIChat/TSChatViewController/TSChatViewController/TSChatViewController.swift

@@ -106,7 +106,7 @@ class TSChatViewController: MessagesViewController, MessagesDataSource {
         //设置自定义FlowLayout,itemsize等,都在这里控制
         let flowLayout = CustomMessagesFlowLayout()
         flowLayout.sectionInset = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0)
-    
+        flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
         messagesCollectionView.collectionViewLayout = flowLayout
         messagesCollectionView.backgroundColor = .clear
         messagesCollectionView.register(TSTextMessageContentCell.self)
@@ -143,10 +143,12 @@ class TSChatViewController: MessagesViewController, MessagesDataSource {
 
     // MARK: - Helpers
     var lastIndexPath:IndexPath{
-        if messageList.count == 0 {
-            return IndexPath(item: 0, section: 0)
+        let section = messagesCollectionView.numberOfSections - 1
+        if messagesCollectionView.numberOfItems(inSection: section) > 0 {
+            let item = messagesCollectionView.numberOfItems(inSection: section) - 1
+            return IndexPath(item: item, section: section)
         }
-        return IndexPath(item: messageList.count - 1, section: 0)
+        return IndexPath(item: 0, section: 0)
     }
     
     func insertMessage(_ message: TSChatMessage) {

+ 0 - 9
AIEmoji/Business/AIChat/TSChatViewController/ViewModel/TSAIChatVM.swift

@@ -103,15 +103,6 @@ extension TSAIChatVM {
         }
     }
     
-//    func updateMessage(msgModel:TSChatMessage){
-//        kExecuteOnMainThread {
-//            //保存数据库
-//            self.dbAIChatList.updateMessage(msgModel: msgModel)
-//            //保存服务器
-//        }
-//    }
-   
-    
     func updateMessages(msgModels:[TSChatMessage]){
         kExecuteOnMainThread {
             //保存数据库

+ 13 - 59
AIEmoji/Business/AIChat/TSChatViewController/Views/TSTextMessageContentCell.swift

@@ -53,7 +53,9 @@ class TSTextMessageContentCell: TSMessageContentCell {
         messageContainerView.addSubview(messageLabel)
         messageLabel.snp.makeConstraints { make in
             make.edges.equalTo(labelEdge)
-            make.width.height.equalTo(0)
+            make.width.lessThanOrEqualTo(300) // 设置最大宽度为 300
+            make.width.greaterThanOrEqualTo(24.0)
+            make.height.greaterThanOrEqualTo(24.0)
         }
         
         messageContainerView.addSubview(activityIndicator)
@@ -89,13 +91,19 @@ class TSTextMessageContentCell: TSMessageContentCell {
         
         //更新frame
         let calculator = sizeCalculator as? TSTextLayoutSizeCalculator
-        let labelFrame = calculator?.messageLabelSize(for: message, at: indexPath,fromCurrentSender: dataSource.isFromCurrentSender(message: message)) ?? TSTextLayoutSizeCalculator.cellMessagelabelMinSize
+        //全面控制 label size
+//        let labelFrame = calculator?.messageLabelSize(for: message, at: indexPath,fromCurrentSender: dataSource.isFromCurrentSender(message: message)) ?? TSTextLayoutSizeCalculator.cellMessagelabelMinSize
+//        messageLabel.snp.updateConstraints { make in
+//            make.width.equalTo(labelFrame.width)
+//            make.height.equalTo(labelFrame.height)
+//        }
+        
+        //label 高度自适应
+        let maxWidth = calculator?.cellMessagelabelMaxWidth(fromCurrentSender: dataSource.isFromCurrentSender(message: message)) ?? TSTextLayoutSizeCalculator.cellMessagelabelMinSize.width
         messageLabel.snp.updateConstraints { make in
-            make.width.equalTo(labelFrame.width)
-            make.height.equalTo(labelFrame.height)
+            make.width.lessThanOrEqualTo(maxWidth)
         }
         
-
         if let msgModel = message as? TSChatMessage {
             //显示旋转的动画
             switch msgModel.sendState {
@@ -124,66 +132,12 @@ class TSTextMessageContentCell: TSMessageContentCell {
             messageLabel.text = text.string
             messageLabel.attributedText = text
 //            debugPrint("attributedText赋值")
-//            extractAndPrintSubstring(from: messageLabel.text ?? "", to: text.string)
-//            appendTextGradually(originalText: messageLabel.text ?? "", newText: text.string)
         default:
             break
         }
     }
-    
-    func appendTextGradually(originalText: String, newText: String) {
-        guard newText.count > originalText.count else { return }
 
-        let startIndex = originalText.endIndex
-        var currentIndex = startIndex
-        let remainingText = newText.suffix(from: startIndex)
-        messageLabel.text = originalText
-        for (index, _) in remainingText.enumerated() {
-            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.05) {
-                let nextIndex = newText.index(after: currentIndex)
-                self.messageLabel.text?.append(newText[currentIndex])
-                if let text = self.messageLabel.text {
-                    self.messageLabel.attributedText = kMDAttributedString(text: text)
-                }
-                currentIndex = nextIndex
-            }
-        }
-    }
-    
-    //延时输出
-    func extractAndPrintSubstring(from sourceString: String, to targetString: String) {
-        // 查找 sourceString 在 targetString 中的结束位置
-        if let range = targetString.range(of: sourceString) {
-            // 获取 sourceString 在 targetString 中的结束位置
-            let startIdx = range.upperBound
-            // 提取从该位置到 targetString 结束的子字符串
-            let extractedSubstring = targetString[startIdx...]
-            let firstString = extractedSubstring.first
- 
-            if let firstString = firstString {
-                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
-                    let text = sourceString + String(firstString)
-                    self.messageLabel.text = text
-                    debugPrint("firstString text =\(text)")
-     
-                    self.messageLabel.attributedText = kMDAttributedString(text: text)
-                    
-                    
-                    
-                    self.extractAndPrintSubstring(from: text, to: targetString)
-                }
-            }else{
-                
-                debugPrint("firstString error =\(firstString)")
-            }
 
-        } else {
-            print("未找到 sourceString")
-            messageLabel.text = targetString
-            messageLabel.attributedText = kMDAttributedString(text: targetString)
-        }
-    }
-    
     func startAnimating() {
         activityIndicator.isHidden = false
         activityIndicator.startAnimating()

+ 3 - 3
AIEmoji/Common/Purchase/TSPurchaseManager.swift

@@ -138,9 +138,9 @@ public class PurchaseManager: NSObject {
     }
 
     @objc public var isVip: Bool {
-//        #if DEBUG
-//        return true
-//        #endif
+        #if DEBUG
+        return true
+        #endif
         guard let expiresDate = expiredDate else {
             return false
         }