// // IMAIMsgContentView.swift // Crush // // Created by Leon on 2025/8/20. // import UIKit import ActiveLabel class IMAIMsgContentConfig: IMContentBaseConfig { override func contentSize(model: SessionBaseModel) -> CGSize { guard model.v2msg != nil else { return .zero } let contentView = IMAIMsgContentView.init(frame: .zero) let content = contentView.contentWith(model: model) // Way 1 to calculate //contentView.contentLabel.attributedText = contentView.formatAttrubuteString(string: content) //var size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude)) // Way 2 to calculate✅ let attributedString = contentView.formatAttrubuteString(string: content) var size = attributedString.boundingRect( with: CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).size let calculatedHeight = ceil(size.height) size.height = calculatedHeight // dlog("最大宽度\(SessionBaseModel.maxBubbleContentWidth) 高度:\(size.height)") if size.height < 20 { size = CGSize(width: size.width, height: 20) } return size } override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { return UIEdgeInsets(top: 0, left: 24, bottom: 8, right: 16) } override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { return UIEdgeInsets(top: 36, left: 16, bottom: 16, right: 16) } override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { return IMAIMsgContentView.self } } class IMAIMsgContentView: IMContentBaseView{ var effectView: UIVisualEffectView! var contentLabel: LineSpaceLabel! // ActiveLabel var audioView : IMAudioFlagView! lazy var audioHelper = IMAudioHelper() required override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setupUI() { effectView = { let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) v.alpha = 1 v.backgroundColor = .c.csedn v.cornerRadius = 16 insertSubview(v, at: 0) v.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0)) } return v }() contentLabel = { //let v = ActiveLabel() //v.font = CLSystemToken.font(token: .tbm) let v = LineSpaceLabel() let typo = CLSystemToken.typography(token: .tbm) v.config(typo) v.textColor = UIColor.c.ctsn v.numberOfLines = 0 v.textColor = .white containerView.addSubview(v) v.snp.makeConstraints { make in make.edges.equalToSuperview() } return v }() audioView = { let v = IMAudioFlagView() v.topButton.addTarget(self, action: #selector(tapAudioButton), for: .touchUpInside) addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview() make.top.equalToSuperview().offset(4) // -12 } return v }() contentLabel.text = "" //contentLabel.textColor = .white // Long press gesture let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:))) addGestureRecognizer(longPressGesture) } func contentWith(model: SessionBaseModel) -> String { var content: String? = nil let message = model.v2msg if String.realEmpty(str: content) { content = message?.text } // #warning("test") // content = "(Watching her parents toast you respectfully, I feel very uncomfortable. After all, she has been standing on the top of the magic capital since she was a child. She has never seen her parents like this, but should I say that he is really handsome?) Are you?" // dlog("☁️content:\(String(describing: content)), createTime:\(String(describing: message?.createTime)), modifytime:\(String(describing: message?.modifyTime))") return content ?? "" } func formatAttrubuteString(string: String) -> NSMutableAttributedString{ let content = string let basic = [NSAttributedString.Key.font: UIFont.t.tbm, NSAttributedString.Key.foregroundColor: UIColor.white, ] let aStr = NSMutableAttributedString(string: content, attributes: basic) //content.withAttributes([.font(.t.tbm), .textColor(.text)]) let ranges = String.findBracketRanges(in: content) let att = [NSAttributedString.Key.foregroundColor: UIColor.c.ctsn] for range in ranges { aStr.addAttributes(att, range: range) } return aStr } override func refreshModel(model: SessionBaseModel) { super.refreshModel(model: model) let content = contentWith(model:model) // 语速,预估语音时长 var speedrate = 0 if let userSpeed = IMAIViewModel.shared.aiIMInfo?.dialogueSpeechRate, let intSpeed = Int(userSpeed){ speedrate = intSpeed } let duration = audioHelper.calculateAudioDuration(text: content, speechRate: speedrate) audioView.secondsLabel.text = duration.imAIaudioDurationString contentLabel.attributedText = formatAttrubuteString(string: content) audioView.reloadState(with: model.speechModel) if model.autoPlayAudioOnce && model.autoPlayAlreadyPlayed == false{ tapAudioButton() model.autoPlayAlreadyPlayed = true } } // MARK: - Action @objc private func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) { if gesture.state == .began { // dlog("Long press detected") let event = IMEventModel() event.eventType = .aiMsgLongPress event.cellModel = self.model event.senderView = self delegate?.onTapAction(event: event) } } @objc private func tapAudioButton(){ // let text = contentWith(model: self.model) // Generate voice(mp3 base64string) dlog("tap Audio button...") let event = IMEventModel() event.eventType = .playAITextToAudio event.cellModel = self.model event.senderView = self delegate?.onTapAction(event: event) audioView.startLoading() } // MARK: - Helper override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // 先用系统默认的命中检测 let view = super.hitTest(point, with: event) if view != nil { return view } // 遍历子视图,专门处理 UIButton for subview in subviews { // 只处理 UIButton 类型 guard subview is UIButton else { continue } // 把点击点转换到子视图坐标系 let convertedPoint = subview.convert(point, from: self) // 如果落在按钮区域内,就返回按钮 if subview.bounds.contains(convertedPoint) { return subview.hitTest(convertedPoint, with: event) } } return nil } }