// // AIInfoInputView.swift // PDF Reader Pro Edition // // Created by Niehaoyu on 2024/4/17. // import Cocoa import PDFKit class customTextView: NSTextView { var firstResponderHandle: ((_ view: customTextView, _ isFirstResponder: Bool) -> Void)? override func becomeFirstResponder() -> Bool { guard let callBack = self.firstResponderHandle else { return super.becomeFirstResponder() } callBack(self, true) return super.becomeFirstResponder() } override func resignFirstResponder() -> Bool { guard let callBack = self.firstResponderHandle else { return super.resignFirstResponder() } callBack(self, false) return super.resignFirstResponder() } } protocol AIInfoInputViewDelegate: AnyObject { func ai_InputViewDidChooseCurFile(aiInputView: AIInfoInputView) } class AIInfoInputView: NSView, NibLoadable, NSTextFieldDelegate, NSTextViewDelegate { @IBOutlet weak var contendBox: NSBox! @IBOutlet weak var typeEmptyTipView: NSView! @IBOutlet weak var typeEmptyTipLabel: NSTextField! @IBOutlet weak var infoContendView: NSView! @IBOutlet weak var typeHeaderView: NSView! @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var titleTipBtn: KMButton! @IBOutlet weak var stringClearBtn: NSButton! @IBOutlet weak var chooseFileBtnView: NSView! @IBOutlet weak var chooseCurFileBtn: NSButton! @IBOutlet weak var chooseFileBtn: NSButton! @IBOutlet weak var chooseFileViewTopConst: NSLayoutConstraint! @IBOutlet weak var fileSizeTipView: NSView! @IBOutlet weak var fileSizeTipLabel: NSTextField! @IBOutlet weak var placeholdLabel: NSTextField! @IBOutlet weak var placeholdLabelTopConst: NSLayoutConstraint! @IBOutlet weak var fileInfoContendView: NSView! @IBOutlet weak var fileInfoImg: NSImageView! @IBOutlet weak var fileNameLabel: NSTextField! @IBOutlet weak var fileSizeLabel: NSTextField! @IBOutlet weak var filePageCountLabel: NSTextField! @IBOutlet weak var removeChooseFileBtn: NSButton! @IBOutlet weak var fileInfoViewTopConst: NSLayoutConstraint! @IBOutlet weak var textScrollView: NSScrollView! @IBOutlet var fileEmptyTextView: customTextView! @IBOutlet weak var textScrollViewTopConst: NSLayoutConstraint! @IBOutlet weak var inputTextCountLabel: NSTextField! @IBOutlet weak var translateConfigView: NSView! @IBOutlet weak var fromLanguageView: NSView! @IBOutlet weak var fromLanguageLabel: NSTextField! @IBOutlet weak var fromLanguageBtn: NSButton! @IBOutlet weak var toLanguageView: NSView! @IBOutlet weak var toLanguageLabel: NSTextField! @IBOutlet weak var toLanguageBtn: NSButton! @IBOutlet weak var translateChangeBtn: KMButton! @IBOutlet weak var startButton: NSButton! weak var aiDelegate: AIInfoInputViewDelegate? var area: NSTrackingArea? var popOver: NSPopover! var aiConfigType: AIConfigType = .none var filePath: String = "" var fromLanguage: String = "" var toLanguage: String = "" var startAIHandle: ((_ view: AIInfoInputView) -> Void)? var inputFrameUpdateHandle: ((_ view: AIInfoInputView, _ stringSize: CGSize) -> Void)? override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. } override func awakeFromNib() { super.awakeFromNib() self.contendBox.borderWidth = 1 self.contendBox.cornerRadius = 7 self.titleLabel.font = NSFont.SFProTextSemiboldFont(13) self.titleTipBtn.mouseMoveCallback = {[unowned self] mouseEntered in if mouseEntered { var tipString = NSLocalizedString("Recommended file size: 10M or less.", comment: "") if self.aiConfigType == .reWriting { tipString = NSLocalizedString("No more than 2000 characters.", comment: "") } else if self.aiConfigType == .proofreading { tipString = NSLocalizedString("No more than 2000 characters.", comment: "") } else if self.aiConfigType == .translate { tipString = NSLocalizedString("1 credit for every 10,000 characters; No more than 10M of a document. ", comment: "") } let popViewController = KMToolbarItemPopViewController.init() if self.popOver == nil { self.popOver = NSPopover.init() } self.popOver.contentViewController = popViewController self.popOver.animates = false self.popOver.behavior = .semitransient self.popOver.contentSize = (popViewController.view.frame.size) popViewController.updateWithHelpTip(helpTip: tipString) let origin = self.titleTipBtn.superview?.convert(self.titleTipBtn.frame.origin, to: self) var frame = self.titleTipBtn.frame frame.origin.x = origin!.x + 12 frame.origin.y = origin!.y + 20 self.popOver.show(relativeTo: frame, of: (self.window?.contentView)!, preferredEdge: .maxY) } else { self.popOver.close() } } let countFormatter = TextFieldFormatter.init() countFormatter.setMaximumLength(2000) self.inputTextCountLabel.formatter = countFormatter self.inputTextCountLabel.delegate = self self.fileEmptyTextView.font = NSFont.SFProTextRegularFont(14) self.fileEmptyTextView.delegate = self self.fileEmptyTextView.firstResponderHandle = {[unowned self] view, isFirstResponder in if isFirstResponder { if self.fileSizeTipView.isHidden == false { self.fileSizeTipView.isHidden = true NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideFileSizeTipView), object: nil) } } } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { self.setUpTranslateUI() } self.fileSizeTipView.isHidden = true self.fileSizeTipView.wantsLayer = true self.fileSizeTipLabel.font = NSFont.SFProTextRegularFont(12) self.fileSizeTipLabel.textColor = KMAppearance.KMColor_Layout_H0() self.fileSizeTipLabel.stringValue = NSLocalizedString("Please upload a file smaller than 10M.", comment: "") self.fileSizeTipView.layer?.cornerRadius = 2 self.fileSizeTipView.layer?.masksToBounds = true self.fileInfoContendView.wantsLayer = true self.fileInfoContendView.layer?.cornerRadius = 6 self.fileInfoContendView.layer?.masksToBounds = true self.translateConfigView.wantsLayer = true self.translateConfigView.layer?.backgroundColor = NSColor.clear.cgColor self.typeEmptyTipLabel.stringValue = NSLocalizedString("Select the AI tool", comment: "") self.chooseCurFileBtn.title = NSLocalizedString("Current File", comment: "") self.chooseFileBtn.title = NSLocalizedString("Choose", comment: "") self.translateChangeBtn.mouseMoveCallback = {[weak self] mouseEntered in if mouseEntered { self?.translateChangeBtn.image = NSImage(named: "AIchange_hover") } else { self?.translateChangeBtn.image = NSImage(named: "AIchange") } } self.updateCountLabelInfo() } override func updateTrackingAreas() { super.updateTrackingAreas() if let _area = self.area, _area.rect.isEmpty == false { if (_area.rect.equalTo(self.bounds)) { return } } if (self.area != nil) { self.removeTrackingArea(self.area!) self.area = nil } self.area = NSTrackingArea(rect: self.bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways], owner: self) self.addTrackingArea(self.area!) } func setUpTranslateUI() { var languages = ["Automatic", "English", "Simplified Chinese", "Traditional Chinese", "Japanese", "Korean", "French", "Spanish", "Italian", "German", "Portuguese", "Russian", "Vietnamese", "Thai", "Arabic", "Greek", "Bulgarian", "Finnish", "Slovene", "Dutch", "Czech", "Swedish", "Polish", "Danish", "Romanian", "Hungarian"] let menu = NSMenu.init() for idx in 0...languages.count-1 { let string = languages[idx] let menuItem = NSMenuItem.init(title: string, action: #selector(menuItemClick(_:)), keyEquivalent: "") menuItem.tag = 1000 + idx menuItem.target = self menu.addItem(menuItem) } self.fromLanguageView?.menu = menu self.fromLanguage = "Automatic" languages = ["English", "Simplified Chinese", "Traditional Chinese", "Japanese", "Korean", "French", "Spanish", "Italian", "German", "Portuguese", "Russian", "Vietnamese", "Thai", "Arabic", "Greek", "Bulgarian", "Finnish", "Slovene", "Dutch", "Czech", "Swedish", "Polish", "Danish", "Romanian", "Hungarian"] let toMenu = NSMenu.init() for idx in 0...languages.count-1 { let string = languages[idx] let menuItem = NSMenuItem.init(title: string, action: #selector(menuItemClick(_:)), keyEquivalent: "") menuItem.tag = 3000 + idx menuItem.target = self toMenu.addItem(menuItem) } self.toLanguageView?.menu = toMenu self.toLanguage = "English" self.fromLanguageLabel.font = NSFont.SFProTextRegularFont(13) self.toLanguageLabel.font = NSFont.SFProTextRegularFont(13) self.fromLanguageLabel.textColor = KMAppearance.KMColor_Layout_H0() self.toLanguageLabel.textColor = KMAppearance.KMColor_Layout_H0() self.fromLanguageLabel.stringValue = self.fromLanguage self.toLanguageLabel.stringValue = self.toLanguage } func aiFunctionTypeChanged() { self.refreshStringSize() } func reloadData() { if AIChatInfoManager.defaultManager.currentFilePath.isEmpty || AIChatInfoManager.defaultManager.isAILoading { self.chooseCurFileBtn.isEnabled = false } else { self.chooseCurFileBtn.isEnabled = true } if AIChatInfoManager.defaultManager.isAILoading { self.chooseFileBtn.isEnabled = false } else { self.chooseFileBtn.isEnabled = true } self.startButton.title = NSLocalizedString("Start (1 credit)", comment: "") if self.aiConfigType == .none { self.typeEmptyTipView.isHidden = false self.infoContendView.isHidden = true } else { self.typeEmptyTipView.isHidden = true self.infoContendView.isHidden = false self.chooseFileBtnView.isHidden = true self.inputTextCountLabel.isHidden = false self.placeholdLabel.isHidden = true self.textScrollView.isHidden = true self.translateConfigView.isHidden = true self.fileInfoContendView.isHidden = true self.removeChooseFileBtn.isHidden = true self.startButton.isEnabled = false self.placeholdLabelTopConst.constant = -2 self.chooseFileViewTopConst.constant = 0 self.textScrollViewTopConst.constant = 0 self.fileInfoViewTopConst.constant = 8 self.stringClearBtn.isHidden = true if self.fileEmptyTextView.string.isEmpty == false { self.stringClearBtn.isHidden = false } if self.aiConfigType == .summarize { self.inputTextCountLabel.isHidden = true self.placeholdLabel.isHidden = false self.chooseFileBtnView.isHidden = false self.placeholdLabel.stringValue = NSLocalizedString("Summarize the current file or click choose other files.", comment: "") self.placeholdLabelTopConst.constant = NSHeight(self.chooseFileBtnView.frame) + 4 if self.filePath.isEmpty == false { self.fileInfoContendView.isHidden = false self.placeholdLabel.isHidden = true let filePathURL = URL(fileURLWithPath: self.filePath) self.fileNameLabel.stringValue = filePathURL.lastPathComponent self.fileSizeLabel.stringValue = self.fileSizeString(Float(self.getFileSize(atPath: self.filePath))) if let document = PDFDocument(url: URL(fileURLWithPath: filePath)) { self.filePageCountLabel.stringValue = String(format: "%d ", document.pageCount) + NSLocalizedString("Page", comment: "") if document.pageCount > 1 { self.filePageCountLabel.stringValue = String(format: "%d ", document.pageCount) + NSLocalizedString("Pages", comment: "") } } else { self.filePageCountLabel.stringValue = "" } self.startButton.isEnabled = true } } else if self.aiConfigType == .reWriting { self.textScrollView.isHidden = false self.placeholdLabel.isHidden = false if self.fileEmptyTextView.string.isEmpty == false { self.placeholdLabel.isHidden = true self.startButton.isEnabled = true } self.placeholdLabel.stringValue = NSLocalizedString("Enter or paste content here...", comment: "") } else if self.aiConfigType == .proofreading { self.textScrollView.isHidden = false self.placeholdLabel.isHidden = false if self.fileEmptyTextView.string.isEmpty == false { self.placeholdLabel.isHidden = true self.startButton.isEnabled = true } self.placeholdLabel.stringValue = NSLocalizedString("Enter or paste content here...", comment: "") } else if self.aiConfigType == .translate { self.textScrollView.isHidden = false self.translateConfigView.isHidden = false self.chooseFileBtnView.isHidden = false self.chooseFileViewTopConst.constant = 30 self.placeholdLabel.isHidden = false self.placeholdLabelTopConst.constant = 58 self.textScrollViewTopConst.constant = 58 self.placeholdLabel.stringValue = NSLocalizedString("Enter or paste content here...", comment: "") if self.filePath.isEmpty == false { self.startButton.title = NSLocalizedString("Start", comment: "") self.fileEmptyTextView.string = "" self.fileInfoContendView.isHidden = false self.placeholdLabel.isHidden = true self.inputTextCountLabel.isHidden = true self.fileInfoViewTopConst.constant = 8 let filePathURL = URL(fileURLWithPath: self.filePath) self.fileNameLabel.stringValue = filePathURL.lastPathComponent self.fileSizeLabel.stringValue = self.fileSizeString(Float(self.getFileSize(atPath: self.filePath))) if let document = PDFDocument(url: URL(fileURLWithPath: filePath)) { self.filePageCountLabel.stringValue = String(format: "%d ", document.pageCount) + NSLocalizedString("Page", comment: "") if document.pageCount > 1 { self.filePageCountLabel.stringValue = String(format: "%d ", document.pageCount) + NSLocalizedString("Pages", comment: "") } } else { self.filePageCountLabel.stringValue = "" } self.startButton.isEnabled = true } else { self.startButton.title = NSLocalizedString("Start (1 credit)", comment: "") self.textScrollView.isHidden = false if self.fileEmptyTextView.string.isEmpty { self.placeholdLabel.isHidden = false self.startButton.isEnabled = false } else { self.placeholdLabel.isHidden = true self.startButton.isEnabled = true } } } } self.refreshUI() } func refreshUI() { if KMAppearance.isDarkMode() { self.titleLabel.textColor = NSColor.white self.contendBox.fillColor = NSColor.white.withAlphaComponent(0.05) self.contendBox.borderColor = KMAppearance.KMColor_Interactive_M0().withAlphaComponent(1) self.fileSizeTipView.layer?.backgroundColor = NSColor(red: 251/255, green: 166/255, blue: 0, alpha: 1).cgColor self.typeEmptyTipLabel.textColor = KMAppearance.KMColor_Interactive_S1() self.placeholdLabel.textColor = KMAppearance.KMColor_Interactive_S1() self.toLanguageView.layer?.backgroundColor = NSColor(red: 110/255, green: 109/255, blue: 112/255, alpha: 1).cgColor self.fromLanguageView.layer?.backgroundColor = NSColor(red: 110/255, green: 109/255, blue: 112/255, alpha: 1).cgColor } else { self.titleLabel.textColor = NSColor.black self.contendBox.borderColor = NSColor(red: 201/255, green: 218/255, blue: 247/255, alpha: 1) self.fileSizeTipView.layer?.backgroundColor = NSColor(red: 253/255, green: 239/255, blue: 212/255, alpha: 1).cgColor self.typeEmptyTipLabel.textColor = KMAppearance.KMColor_Layout_B30() self.placeholdLabel.textColor = KMAppearance.KMColor_Layout_B30() self.toLanguageView.layer?.backgroundColor = NSColor(red: 236/255, green: 242/255, blue: 254/255, alpha: 1).cgColor self.fromLanguageView.layer?.backgroundColor = NSColor(red: 236/255, green: 242/255, blue: 254/255, alpha: 1).cgColor } if self.aiConfigType == .summarize { self.titleLabel.stringValue = "#" + NSLocalizedString("AI Summarize", comment: "") if KMAppearance.isDarkMode() { self.titleLabel.textColor = NSColor(red: 85/255, green: 245/255, blue: 1, alpha: 1) } else { self.titleLabel.textColor = NSColor(red: 0, green: 209/255, blue: 222/255, alpha: 1) } } else if self.aiConfigType == .reWriting { self.titleLabel.stringValue = "#" + NSLocalizedString("AI Rewrite", comment: "") if KMAppearance.isDarkMode() { self.titleLabel.textColor = NSColor(red: 255/255, green: 105/255, blue: 195/255, alpha: 1) } else { self.titleLabel.textColor = NSColor(red: 240/255, green: 28/255, blue: 155/255, alpha: 1) } } else if self.aiConfigType == .proofreading { self.titleLabel.stringValue = "#" + NSLocalizedString("AI Proofread", comment: "") if KMAppearance.isDarkMode() { self.titleLabel.textColor = NSColor(red: 194/255, green: 157/255, blue: 1, alpha: 1) } else { self.titleLabel.textColor = NSColor(red: 108/255, green: 28/255, blue: 240/255, alpha: 1) } } else if self.aiConfigType == .translate { self.titleLabel.stringValue = "#" + NSLocalizedString("AI Translate", comment: "") if KMAppearance.isDarkMode() { self.titleLabel.textColor = NSColor(red: 255/255, green: 152/255, blue: 77/255, alpha: 1) } else { self.titleLabel.textColor = NSColor(red: 240/255, green: 101/255, blue: 0, alpha: 1) } } self.placeholdLabel.font = NSFont.SFProTextRegularFont(14) self.fromLanguageView.wantsLayer = true self.toLanguageView.wantsLayer = true } func updateCountLabelInfo() { self.inputTextCountLabel.stringValue = String(format: "%ld", self.fileEmptyTextView.string.count) + "/2000" if self.fileEmptyTextView.string.count == 2000 { self.inputTextCountLabel.textColor = KMAppearance.KMColor_Status_Err() } else { if KMAppearance.isDarkMode() { self.inputTextCountLabel.textColor = KMAppearance.KMColor_Layout_W30() } else { self.inputTextCountLabel.textColor = KMAppearance.KMColor_Layout_B30() } } } func getFileSize(atPath filePath : String) -> CGFloat { guard let dict = try? FileManager.default.attributesOfItem(atPath: filePath) as NSDictionary else { return 0 } return CGFloat(dict.fileSize()) } func fileSizeString(_ fSize: Float) -> String { let fileSize = fSize / 1024 let size = fileSize >= 1024 ? (fileSize < 1048576 ? fileSize/1024 : fileSize/1048576.0) : fileSize let unit = fileSize >= 1024 ? (fileSize < 1048576 ? "M" : "G") : "K" return String(format: "%0.1f %@", size, unit) } @objc func hideFileSizeTipView() { self.fileSizeTipView.isHidden = true } //MARK: IBAction @objc func menuItemClick(_ item: NSMenuItem) { if item.tag < 2000 { self.fromLanguage = item.title } else { self.toLanguage = item.title } self.fromLanguageLabel.stringValue = self.fromLanguage self.toLanguageLabel.stringValue = self.toLanguage } @IBAction func chooseCurFileAction(_ sender: Any) { self.aiDelegate?.ai_InputViewDidChooseCurFile(aiInputView: self) self.fileSizeTipView.isHidden = true let curFilePath = AIChatInfoManager.defaultManager.currentFilePath let fileSize = self.getFileSize(atPath: curFilePath) if fileSize/(1024*1024) > 10 { self.fileSizeTipView.isHidden = false NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideFileSizeTipView), object: nil) self.perform(#selector(hideFileSizeTipView), with: nil, afterDelay: 5) self.filePath = "" self.fileEmptyTextView.string = "" self.reloadData() } else { self.filePath = curFilePath self.fileEmptyTextView.string = "" self.reloadData() } self.mouseEntered(with: NSEvent()) } @IBAction func summaryUploadAction(_ sender: NSButton) { self.fileSizeTipView.isHidden = true let openPanel = NSOpenPanel() openPanel.canChooseDirectories = false openPanel.canChooseFiles = true openPanel.allowsMultipleSelection = false; openPanel.allowedFileTypes = ["pdf","PDF"] openPanel.beginSheetModal(for: self.window!) { [self] result in if result == .OK { let fileURL = openPanel.urls.first let fileSize = self.getFileSize(atPath: fileURL!.path) if fileSize/(1024*1024) > 10 { self.fileSizeTipView.isHidden = false NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideFileSizeTipView), object: nil) self.perform(#selector(hideFileSizeTipView), with: nil, afterDelay: 5) self.filePath = "" self.fileEmptyTextView.string = "" self.reloadData() } else { self.filePath = fileURL!.path self.fileEmptyTextView.string = "" self.reloadData() } } } } @IBAction func chooseLanguageAction(_ sender: NSButton) { if sender == self.fromLanguageBtn { let menu = self.fromLanguageView?.menu menu?.popUp(positioning: menu?.item(at: 0), at: CGPoint(x: 0, y: 15), in: sender) } else if sender == self.toLanguageBtn { let menu = self.toLanguageView?.menu menu?.popUp(positioning: menu?.item(at: 0), at: CGPoint(x: 0, y: 15), in: sender) } } @IBAction func languageChangeAction(_ sender: Any) { let curLan = self.fromLanguage self.fromLanguage = self.toLanguage self.toLanguage = curLan self.fromLanguageLabel.stringValue = self.fromLanguage self.toLanguageLabel.stringValue = self.toLanguage } @IBAction func removeChooseFile(_ sender: NSButton) { if self.aiConfigType == .summarize || self.aiConfigType == .translate { self.filePath = "" self.reloadData() self.updateCountLabelInfo() } } @IBAction func clearInputStringAction(_ sender: Any) { self.fileEmptyTextView.string = "" self.reloadData() } @IBAction func startAIAction(_ sender: NSButton) { let newStatus: Bool = KMCloudServer.isConnectionAvailable() if !newStatus { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("Please make sure your internet connection is available.", comment: "") alert.runModal() return } guard let callBack = self.startAIHandle else { return } callBack(self) if self.aiConfigType == .summarize { self.filePath = "" } else if self.aiConfigType == .reWriting { self.fileEmptyTextView.string = "" } else if self.aiConfigType == .proofreading { self.fileEmptyTextView.string = "" } else if self.aiConfigType == .translate { self.filePath = "" self.fileEmptyTextView.string = "" } self.reloadData() self.updateCountLabelInfo() self.refreshStringSize() } func sizeOfString(_ string: String, _ font: NSFont) -> (CGSize) { let attributes: [NSAttributedString.Key: Any] = [ .font: font ] let size = (string as NSString).boundingRect(with: NSSize(width: NSWidth(self.textScrollView.frame), height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size return size } func refreshStringSize() { var size = self.sizeOfString(self.fileEmptyTextView.string, self.fileEmptyTextView.font!) if self.aiConfigType == .summarize { if self.filePath.isEmpty == true { size = self.sizeOfString(self.placeholdLabel.stringValue, self.placeholdLabel.font!) } } guard let callBack = self.inputFrameUpdateHandle else { return } callBack(self, size) } //MARK: Deletegate func textShouldBeginEditing(_ textObject: NSText) -> Bool { print("textShouldBeginEditing") return true } func textDidChange(_ notification: Notification) { if let textView = notification.object as? NSTextView, textView == self.fileEmptyTextView { // 获取文本字段的当前字符数 let currentText = textView.string let currentCount = currentText.count // 如果超过最大字符数,将文本截断为最大字符数,并设置回文本字段 if currentCount > 2000 { let endIndex = currentText.index(currentText.startIndex, offsetBy: 2000) let truncatedText = String(currentText[..