From c37df9fb945922786cfacab475eae1b0d5cdba19 Mon Sep 17 00:00:00 2001 From: Lakr <25259084+Lakr233@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:30:00 +0800 Subject: [PATCH] chore: made attachment header & management sheet (#12922) ## Summary by CodeRabbit - **New Features** - Introduced file, image, and document attachment support in the input box, including new UI components for managing and previewing attachments. - Added a searchable document picker view and a file attachment header with interactive management options. - Enabled an attachment management controller for viewing and deleting attachments. - Improved image attachment bar with horizontal scrolling and removal functionality. - Enhanced error handling for file attachments, providing user-facing alerts. - **Improvements** - Updated attachment menus for clearer file type indications. - Streamlined attachment handling logic and UI updates for a smoother user experience. - **Bug Fixes** - Addressed error notification by replacing console logging with user alerts when file attachment issues occur. - **Refactor** - Replaced and reorganized the input box view model and attachment bar for better modularity and maintainability. - **Chores** - Updated asset catalogs to include new attachment icons for various file types. --- .../App/Packages/Intelligents/Package.swift | 4 +- .../Intelligents/Extension/UIColor.swift | 19 ++ .../Intelligents/Extension/ViewPreview.swift | 52 +++ .../AttachmentIcon.xcassets/Contents.json | 6 + .../FileAttachment.imageset/Contents.json | 12 + .../FileAttachment.imageset/Page.pdf | Bin 0 -> 3972 bytes .../AiFileClip attachment icon.pdf | Bin 0 -> 8385 bytes .../Contents.json | 12 + .../AiFileClip attachment icon.pdf | Bin 0 -> 6386 bytes .../FileAttachment_md.imageset/Contents.json | 12 + .../AiFileClip attachment icon.pdf | Bin 0 -> 4994 bytes .../FileAttachment_pdf.imageset/Contents.json | 12 + .../AiFileClip attachment icon.pdf | Bin 0 -> 6140 bytes .../FileAttachment_txt.imageset/Contents.json | 12 + .../AttachmentManagementController.swift | 306 ++++++++++++++++++ .../MainViewController+Input.swift | 10 +- .../DocumentPickerView.swift | 205 ++++++++++++ .../DocumentTableViewCell.swift | 62 ++++ .../FileAttachmentHeaderView.swift | 171 ++++++++++ .../ImageAttachmentBar.swift | 156 +++++++++ .../ImageCollectionViewCell.swift | 61 ++++ .../Interface/View/InputBox/InputBox.swift | 124 +++---- .../View/InputBox/InputBoxDelegate.swift | 68 +++- .../View/InputBox/InputBoxFunctionBar.swift | 2 +- .../View/InputBox/InputBoxImageBar.swift | 180 ----------- .../View/InputBox/InputBoxViewModel.swift | 164 ---------- .../ViewModel/DocumentAttachment.swift | 15 + .../InputBox/ViewModel/FileAttachment.swift | 28 ++ .../InputBox/ViewModel/ImageAttachment.swift | 17 + .../ViewModel/InputBoxViewModel.swift | 159 +++++++++ 30 files changed, 1460 insertions(+), 409 deletions(-) create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/ViewPreview.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Page.pdf create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/AiFileClip attachment icon.pdf create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_md.imageset/AiFileClip attachment icon.pdf create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_md.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_pdf.imageset/AiFileClip attachment icon.pdf create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_pdf.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/AiFileClip attachment icon.pdf create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageCollectionViewCell.swift delete mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift delete mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/FileAttachment.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/ImageAttachment.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/InputBoxViewModel.swift diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index f3b46225a..338097ebd 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -10,7 +10,7 @@ let package = Package( .iOS(.v16), ], products: [ - .library(name: "Intelligents", targets: ["Intelligents"]), + .library(name: "Intelligents", type: .dynamic, targets: ["Intelligents"]), ], dependencies: [ .package(path: "../AffineGraphQL"), @@ -29,8 +29,8 @@ let package = Package( .product(name: "Apollo", package: "apollo-ios"), .product(name: "OrderedCollections", package: "swift-collections"), ], resources: [ - .process("Resources/main.metal"), .process("Interface/View/InputBox/InputBox.xcassets"), + .process("Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets"), ]), ] ) diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor.swift new file mode 100644 index 000000000..7d77698f7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor.swift @@ -0,0 +1,19 @@ +// +// UIColor.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import UIKit + +private extension UIColor { + convenience init(hex: Int, alpha: CGFloat = 1.0) { + self.init( + red: CGFloat((hex & 0xFF0000) >> 16) / 255.0, + green: CGFloat((hex & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(hex & 0x0000FF) / 255.0, + alpha: alpha + ) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/ViewPreview.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/ViewPreview.swift new file mode 100644 index 000000000..b48f2b0d2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/ViewPreview.swift @@ -0,0 +1,52 @@ +// +// ViewPreview.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import Foundation +import UIKit + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + struct UIViewControllerPreview: UIViewControllerRepresentable { + let viewController: ViewController + + init(_ builder: @escaping () -> ViewController) { + viewController = builder() + } + + func makeUIViewController(context _: Context) -> ViewController { + viewController + } + + func updateUIViewController(_: ViewController, context _: Context) {} + } +#endif + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + struct UIViewPreview: UIViewRepresentable { + let view: View + + init(_ builder: @escaping () -> View) { + view = builder() + } + + // MARK: UIViewRepresentable + + func makeUIView(context _: Context) -> UIView { + view + } + + func updateUIView(_ view: UIView, context _: Context) { + view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal) + view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical) + view.setContentHuggingPriority(.defaultHigh, for: .horizontal) + view.setContentHuggingPriority(.defaultHigh, for: .vertical) + } + } +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Contents.json new file mode 100644 index 000000000..6f5c8f9cd --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Page.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Page.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment.imageset/Page.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cb33cb731628542d5f213a578243eb8755bc24f5 GIT binary patch literal 3972 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 UqudBTDj@?$KtZQZYh2O%55N-IV*mgE literal 0 HcmV?d00001 diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/AiFileClip attachment icon.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/AiFileClip attachment icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1715e60b211be70e2a2e5187fe644c65e48e6fa2 GIT binary patch literal 8385 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/Contents.json new file mode 100644 index 000000000..bb48b846f --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AiFileClip attachment icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_md.imageset/AiFileClip attachment icon.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_md.imageset/AiFileClip attachment icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a0ab4c6cc0f8fe9eb7562bb10fa648f6214d8527 GIT binary patch literal 6386 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 OR16{lMhM|mvSbM*TO>;;*~?Oh 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 GTK@r=UVq{M literal 0 HcmV?d00001 diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/Contents.json new file mode 100644 index 000000000..bb48b846f --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AiFileClip attachment icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift new file mode 100644 index 000000000..844720f1b --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift @@ -0,0 +1,306 @@ +// +// AttachmentManagementController.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import SnapKit +import Then +import UIKit + +protocol AttachmentManagementControllerDelegate: AnyObject { + func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) + func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) +} + +class AttachmentManagementController: UINavigationController { + private let _viewController: _AttachmentManagementController + init(delegate: AttachmentManagementControllerDelegate) { + let attachmentManagementController = _AttachmentManagementController(delegate: delegate) + _viewController = attachmentManagementController + super.init(rootViewController: attachmentManagementController) + _viewController.delegateController = self + navigationBar.isHidden = false + modalPresentationStyle = .formSheet + modalTransitionStyle = .coverVertical + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func set(documentAttachments attachments: [DocumentAttachment]) { + _ = _viewController.view // trigger view did load + _viewController.documentAttachments = attachments + } + + func set(fileAttachments attachments: [FileAttachment]) { + _ = _viewController.view // trigger view did load + _viewController.fileAttachments = attachments + } +} + +private class _AttachmentManagementController: UIViewController { + weak var delegateController: AttachmentManagementController? + weak var delegate: AttachmentManagementControllerDelegate? + private let tableView: UITableView = .init(frame: .zero, style: .plain) + private lazy var dataSource: UITableViewDiffableDataSource< + Section, + Item + > = .init(tableView: tableView) { [weak self] tableView, indexPath, item in + let cell = tableView.dequeueReusableCell(withIdentifier: "AttachmentCell", for: indexPath) as! AttachmentCell + cell.configure(with: item) + cell.onDelete = { [weak self] in + guard let delegateController = self?.delegateController else { return } + switch item.type { + case let .file(file): + self?.delegate?.deleteFileAttachment(controller: delegateController, file) + case let .document(doc): + self?.delegate?.deleteDocumentAttachment(controller: delegateController, doc) + } + } + return cell + } + + enum Section: Int, CaseIterable { + case files + case documents + } + + struct Item: Hashable { + let id: UUID + let title: String + let icon: UIImage? + let type: ItemType + enum ItemType: Hashable { + case file(FileAttachment) + case document(DocumentAttachment) + } + + static func == (lhs: Item, rhs: Item) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } + + var fileAttachments: [FileAttachment] = [] { + didSet { + NSObject.cancelPreviousPerformRequests(withTarget: self) + perform(#selector(reloadDataSource), with: nil, afterDelay: 0) + } + } + + var documentAttachments: [DocumentAttachment] = [] { + didSet { + NSObject.cancelPreviousPerformRequests(withTarget: self) + perform(#selector(reloadDataSource), with: nil, afterDelay: 0) + } + } + + init(delegate: AttachmentManagementControllerDelegate) { + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + title = "Attachments & Docs" + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + navigationItem.title = "Attachments & Docs" + navigationItem.rightBarButtonItem = .init(systemItem: .done, primaryAction: .init { [weak self] _ in + self?.doneTapped() + }) + + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.register(AttachmentCell.self, forCellReuseIdentifier: "AttachmentCell") + tableView.clipsToBounds = true + tableView.rowHeight = UITableView.automaticDimension + view.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + applySnapshot() + } + + @objc private func doneTapped() { + dismiss(animated: true) + } + + @objc func reloadDataSource() { + applySnapshot() + if fileAttachments.isEmpty, documentAttachments.isEmpty { + dismiss(animated: true) + } + } + + private func applySnapshot() { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.files, .documents]) + let fileItems = fileAttachments.map { file in + Item(id: file.id, title: file.name, icon: file.icon, type: .file(file)) + } + let docItems = documentAttachments.map { doc in + Item(id: doc.id, title: doc.title, icon: .init(named: "FileAttachment", in: .module, with: nil)!, type: .document(doc)) + } + snapshot.appendItems(fileItems, toSection: .files) + snapshot.appendItems(docItems, toSection: .documents) + dataSource.apply(snapshot, animatingDifferences: true) + } +} + +private class AttachmentCell: UITableViewCell { + let container = UIView().then { + $0.layer.cornerRadius = 4 + $0.layer.borderWidth = 0.5 + $0.layer.borderColor = UIColor.affineLayerBorder.cgColor + } + + let iconView = UIImageView().then { + $0.contentMode = .scaleAspectFit + $0.tintColor = .affineIconPrimary + } + + let titleLabel = UILabel().then { + $0.textColor = .label + $0.textAlignment = .left + $0.font = .preferredFont(forTextStyle: .body) + } + + let deleteButton = UIButton(type: .system).then { + $0.setImage(UIImage(systemName: "xmark"), for: .normal) + $0.tintColor = .affineIconPrimary + } + + var onDelete: (() -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + private let inset: CGFloat = 10 + + private func setupUI() { + contentView.addSubview(container) + container.snp.makeConstraints { make in + make.top.bottom.equalToSuperview().inset(4) + make.left.right.equalToSuperview().inset(inset) + } + + container.addSubview(iconView) + container.addSubview(titleLabel) + container.addSubview(deleteButton) + iconView.snp.makeConstraints { make in + make.left.equalToSuperview().inset(inset) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(inset) + make.bottom.lessThanOrEqualToSuperview().offset(-inset) + } + titleLabel.snp.makeConstraints { make in + make.left.equalTo(iconView.snp.right).offset(inset) + make.right.lessThanOrEqualTo(deleteButton.snp.left).offset(-inset) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(inset) + make.bottom.lessThanOrEqualToSuperview().offset(-inset) + } + deleteButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-inset) + make.centerY.equalToSuperview() + make.width.height.equalTo(12) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(inset) + make.bottom.lessThanOrEqualToSuperview().offset(-inset) + } + deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + container.layer.borderColor = UIColor.affineLayerBorder.cgColor + } + + func configure(with item: _AttachmentManagementController.Item) { + iconView.image = item.icon + titleLabel.text = item.title + } + + override func prepareForReuse() { + super.prepareForReuse() + iconView.image = nil + titleLabel.text = nil + onDelete = nil + } + + @objc private func deleteTapped() { + onDelete?() + } +} + +private extension FileAttachment { + var icon: UIImage? { + switch url.pathExtension.lowercased() { + case "pdf": + .init(named: "FileAttachment_pdf", in: .module, with: nil)! + case "json": + .init(named: "FileAttachment_json", in: .module, with: nil)! + case "md": + .init(named: "FileAttachment_md", in: .module, with: nil)! + case "txt": + .init(named: "FileAttachment_txt", in: .module, with: nil)! + default: + .init(named: "FileAttachment", in: .module, with: nil)! + } + } +} + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + private class MockDelegate: AttachmentManagementControllerDelegate { + static let shared = MockDelegate() + func deleteFileAttachment(controller _: AttachmentManagementController, _: FileAttachment) {} + func deleteDocumentAttachment(controller _: AttachmentManagementController, _: DocumentAttachment) {} + } + + struct AttachmentManagementController_Previews: PreviewProvider { + static var previews: some View { + UIViewControllerPreview { + let vc = AttachmentManagementController(delegate: MockDelegate.shared) + let fileAttachments = [ + FileAttachment(url: .init(fileURLWithPath: "/p.pdf"), name: "File 1.pdf"), + FileAttachment(url: .init(fileURLWithPath: "/p.md"), name: "File 2.md"), + FileAttachment(url: .init(fileURLWithPath: "/p.txt"), name: "File 3.txt"), + FileAttachment(url: .init(fileURLWithPath: "/p.json"), name: "File 4.json"), + FileAttachment(url: .init(fileURLWithPath: "/p.xls"), name: "File 4.xls"), + ] + let documentAttachments = [ + DocumentAttachment(title: "Cloud Document A"), + DocumentAttachment(title: "Cloud Document B"), + ] + vc.set(fileAttachments: fileAttachments) + vc.set(documentAttachments: documentAttachments) + return vc + } + .edgesIgnoringSafeArea(.all) + } + } +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift index c1304562d..6f12f88fc 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift @@ -114,9 +114,15 @@ extension MainViewController: UIDocumentPickerDelegate { try FileManager.default.copyItem(at: url, to: tempURL) // Add file attachment using the temporary URL - inputBox.addFileAttachment(tempURL) + try inputBox.addFileAttachment(tempURL) } catch { - print("Failed to copy file: \(error)") + let alert = UIAlertController( + title: "Error", + message: "Failed to process file: \(error.localizedDescription)", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift new file mode 100644 index 000000000..17d9eb951 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift @@ -0,0 +1,205 @@ +// +// DocumentPickerView.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import SnapKit +import Then +import UIKit + +protocol DocumentPickerViewDelegate: AnyObject { + func documentPickerView(_ view: DocumentPickerView, didSelectDocument document: DocumentItem) + func documentPickerView(_ view: DocumentPickerView, didSearchWithText text: String) +} + +struct DocumentItem { + let title: String + let icon: UIImage? +} + +class DocumentPickerView: UIView { + // MARK: - Properties + + weak var delegate: DocumentPickerViewDelegate? + + private var documents: [DocumentItem] = [] + + // MARK: - UI Components + + private lazy var containerView = UIView().then { + $0.backgroundColor = .white + $0.layer.cornerRadius = 10 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.layer.shadowColor = UIColor.black.cgColor + $0.layer.shadowOffset = CGSize(width: 0, height: -3) + $0.layer.shadowRadius = 5 + $0.layer.shadowOpacity = 0.07 + } + + private lazy var searchContainerView = UIView().then { + $0.backgroundColor = .white + $0.layer.cornerRadius = 10 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.layer.borderWidth = 0.5 + $0.layer.borderColor = UIColor(hex: 0xE6E6E6)?.cgColor + } + + private lazy var searchIconImageView = UIImageView().then { + $0.image = UIImage(systemName: "magnifyingglass") + $0.tintColor = UIColor(hex: 0x141414) + $0.contentMode = .scaleAspectFit + } + + private lazy var searchTextField = UITextField().then { + $0.placeholder = "Search documents..." + $0.font = .systemFont(ofSize: 17, weight: .regular) + $0.textColor = UIColor(hex: 0x141414) + $0.backgroundColor = .clear + $0.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged) + } + + private lazy var tableView = UITableView().then { + $0.backgroundColor = .white + $0.separatorStyle = .none + $0.delegate = self + $0.dataSource = self + $0.register(DocumentTableViewCell.self, forCellReuseIdentifier: "DocumentCell") + } + + // MARK: - Initialization + + init() { + super.init(frame: .zero) + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setupUI() { + backgroundColor = .systemBackground + + addSubview(containerView) + containerView.addSubview(searchContainerView) + containerView.addSubview(tableView) + + searchContainerView.addSubview(searchIconImageView) + searchContainerView.addSubview(searchTextField) + + setupConstraints() + } + + private func setupConstraints() { + containerView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.equalTo(393) + make.height.lessThanOrEqualTo(500) + } + + searchContainerView.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.bottom.equalTo(searchIconImageView.snp.bottom).offset(DocumentTableViewCell.cellInset) + make.top.equalTo(searchIconImageView.snp.top).offset(-DocumentTableViewCell.cellInset) + } + + searchIconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(DocumentTableViewCell.cellInset) + make.centerY.equalToSuperview() + make.width.height.equalTo(DocumentTableViewCell.iconSize) + } + + searchTextField.snp.makeConstraints { make in + make.leading.equalTo(searchIconImageView.snp.trailing).offset(DocumentTableViewCell.spacing) + make.trailing.equalToSuperview().offset(-DocumentTableViewCell.cellInset) + make.centerY.equalToSuperview() + } + + tableView.snp.makeConstraints { make in + make.top.equalTo(searchContainerView.snp.bottom) + make.leading.trailing.bottom.equalToSuperview() + } + } + + // MARK: - Public Methods + + func updateDocuments(_ documents: [DocumentItem]) { + self.documents = documents + tableView.reloadData() + + let tableHeight = min(CGFloat(documents.count) * 37.11 + 44, 500) + containerView.snp.updateConstraints { make in + make.height.lessThanOrEqualTo(tableHeight) + } + } + + // MARK: - Actions + + @objc private func searchTextChanged() { + guard let text = searchTextField.text else { return } + delegate?.documentPickerView(self, didSearchWithText: text) + } +} + +// MARK: - UITableViewDataSource + +extension DocumentPickerView: UITableViewDataSource { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + documents.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "DocumentCell", for: indexPath) as! DocumentTableViewCell + cell.configure(with: documents[indexPath.row]) + return cell + } +} + +// MARK: - UITableViewDelegate + +extension DocumentPickerView: UITableViewDelegate { + func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + DocumentTableViewCell.cellInset * 2 + DocumentTableViewCell.iconSize + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let document = documents[indexPath.row] + delegate?.documentPickerView(self, didSelectDocument: document) + } +} + +// MARK: - Preview + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + struct DocumentPickerView_Previews: PreviewProvider { + static var previews: some View { + UIViewPreview { + let view = DocumentPickerView() + + let mockDocuments = [ + DocumentItem(title: "Project Proposal.docx", icon: UIImage(systemName: "doc.text")), + DocumentItem(title: "Budget Analysis.xlsx", icon: UIImage(systemName: "tablecells")), + DocumentItem(title: "Meeting Notes.pdf", icon: UIImage(systemName: "doc.richtext")), + DocumentItem(title: "Design Guidelines.sketch", icon: UIImage(systemName: "paintbrush")), + DocumentItem(title: "Code Review.md", icon: UIImage(systemName: "doc.plaintext")), + DocumentItem(title: "User Research.pptx", icon: UIImage(systemName: "doc.on.doc")), + DocumentItem(title: "Technical Specification.docx", icon: UIImage(systemName: "doc.text")), + DocumentItem(title: "Database Schema.sql", icon: UIImage(systemName: "cylinder.split.1x2")), + ] + + view.updateDocuments(mockDocuments) + return view + } + .previewLayout(.fixed(width: 400, height: 600)) + .previewDisplayName("Document Picker") + } + } +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift new file mode 100644 index 000000000..56802bdbf --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift @@ -0,0 +1,62 @@ +// +// DocumentTableViewCell.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import SnapKit +import Then +import UIKit + +class DocumentTableViewCell: UITableViewCell { + static let cellInset: CGFloat = 16 + static let iconSize: CGFloat = 20 + static let spacing: CGFloat = 16 + + private lazy var iconImageView = UIImageView().then { + $0.contentMode = .scaleAspectFit + $0.tintColor = UIColor(hex: 0x141414) + } + + private lazy var titleLabel = UILabel().then { + $0.font = .systemFont(ofSize: 17, weight: .regular) + $0.textColor = UIColor(hex: 0x141414) + $0.textAlignment = .left + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = .white + selectionStyle = .none + + contentView.addSubview(iconImageView) + contentView.addSubview(titleLabel) + + iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(Self.cellInset) + make.centerY.equalToSuperview() + make.width.height.equalTo(Self.iconSize) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(iconImageView.snp.trailing).offset(Self.spacing) + make.trailing.equalToSuperview().offset(-Self.cellInset) + make.centerY.equalToSuperview() + } + } + + func configure(with document: DocumentItem) { + iconImageView.image = document.icon ?? UIImage(systemName: "doc.text") + titleLabel.text = document.title + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift new file mode 100644 index 000000000..da3455f41 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift @@ -0,0 +1,171 @@ +import SnapKit +import Then +import UIKit + +protocol FileAttachmentHeaderViewDelegate: AnyObject { + func headerViewDidPickMore(_ headerView: FileAttachmentHeaderView) + func headerViewDidTapManagement(_ headerView: FileAttachmentHeaderView) +} + +final class FileAttachmentHeaderView: UIView { + // MARK: - Properties + + weak var delegate: FileAttachmentHeaderViewDelegate? + + // MARK: - UI Components + + private lazy var iconImageView = UIImageView().then { + $0.contentMode = .scaleAspectFit + $0.image = UIImage(systemName: "doc.fill") + $0.tintColor = UIColor.systemBlue + $0.isUserInteractionEnabled = true + let tap = UITapGestureRecognizer(target: self, action: #selector(iconTapped)) + $0.addGestureRecognizer(tap) + } + + private lazy var textStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 2 + $0.alignment = .leading + $0.distribution = .equalSpacing + } + + private lazy var primaryLabel = UILabel().then { + $0.text = "" // 3 attachment, 1 AFFiNE docs + $0.font = UIFont.preferredFont(forTextStyle: .footnote).bold + $0.textColor = .label + $0.numberOfLines = 1 + } + + private lazy var secondaryLabel = UILabel().then { + $0.text = "Referenced for AI" + $0.font = UIFont.preferredFont(forTextStyle: .footnote) + $0.textColor = .affineTextSecondary + $0.numberOfLines = 1 + } + + private lazy var arrowButton = UIImageView().then { + $0.image = UIImage(systemName: "chevron.down") + $0.contentMode = .scaleAspectFit + $0.tintColor = .affineIconPrimary + } + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setupUI() { + backgroundColor = UIColor.white + layer.cornerRadius = 12 + layer.borderWidth = 0.5 + layer.borderColor = UIColor.systemGray5.cgColor + layer.shadowColor = UIColor.black.cgColor + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.shadowRadius = 6 + layer.shadowOpacity = 0.04 + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTapped)) + addGestureRecognizer(tapGesture) + + addSubviews() + setupConstraints() + setupStackView() + } + + private func addSubviews() { + addSubview(iconImageView) + addSubview(textStackView) + addSubview(arrowButton) + } + + private func setupConstraints() { + iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(12) + make.size.equalTo(24) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().inset(12) + make.bottom.lessThanOrEqualToSuperview().inset(12) + } + + textStackView.snp.makeConstraints { make in + make.leading.equalTo(iconImageView.snp.trailing).offset(12) + make.trailing.lessThanOrEqualTo(arrowButton.snp.leading).offset(-12) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().inset(12) + make.bottom.lessThanOrEqualToSuperview().inset(12) + } + + arrowButton.snp.makeConstraints { make in + make.trailing.equalToSuperview().offset(-12) + make.centerY.equalToSuperview() + make.width.equalTo(18) + make.height.equalTo(18) + } + } + + private func setupStackView() { + textStackView.addArrangedSubview(primaryLabel) + textStackView.addArrangedSubview(secondaryLabel) + } + + // MARK: - Actions + + @objc private func viewTapped() { + delegate?.headerViewDidTapManagement(self) + } + + @objc private func iconTapped() { + delegate?.headerViewDidPickMore(self) + } + + // MARK: - Public Methods + + func updateContent(attachmentCount: Int, docsCount: Int) { + primaryLabel.text = "\(attachmentCount) attachment, \(docsCount) AFFiNE docs" + } + + func setIconImage(_ image: UIImage?) { + iconImageView.image = image + } + + // MARK: - Trait Collection + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + layer.borderColor = UIColor.systemGray5.cgColor + } + } +} + +// MARK: - Preview + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + struct FileAttachmentHeaderView_Previews: PreviewProvider { + static var previews: some View { + UIViewPreview { + let view = FileAttachmentHeaderView() + view.updateContent(attachmentCount: 5, docsCount: 2) + view.snp.makeConstraints { make in + make.width.equalTo(400) + } + return view + } + .previewLayout(.fixed(width: 400, height: 100)) + .previewDisplayName("File Attachment Header") + } + } +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift new file mode 100644 index 000000000..8519b6cec --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift @@ -0,0 +1,156 @@ +// +// ImageAttachmentBar.swift +// Intelligents +// +// Created by 秋星桥 on 6/18/25. +// + +import SnapKit +import Then +import UIKit + +protocol ImageAttachmentBarDelegate: AnyObject { + func inputBoxImageBar(_ imageBar: ImageAttachmentBar, didRemoveImageWithId id: UUID) +} + +class ImageAttachmentBar: UICollectionView { + weak var imageBarDelegate: ImageAttachmentBarDelegate? + + enum Section { + case main + } + + private var attachments: [ImageAttachment] = [] + private let cellSpacing: CGFloat = 8 + private let constantHeight: CGFloat = 80 + + var myDataSource: UICollectionViewDiffableDataSource< + Section, + ImageAttachment + >! + + init(frame: CGRect = .zero) { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: 80, height: 80) + layout.minimumInteritemSpacing = 8 + layout.minimumLineSpacing = 8 + layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + + super.init(frame: frame, collectionViewLayout: layout) + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + backgroundColor = .clear + + setupDataSource() + delegate = self + register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: "ImageCell") + + snp.makeConstraints { make in + make.height.equalTo(constantHeight) + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func updateImageBarContent(_ attachments: [ImageAttachment]) { + self.attachments = attachments + applySnapshot() + } + + func clear() { + attachments.removeAll() + applySnapshot() + } + + private func setupDataSource() { + myDataSource = .init(collectionView: self) { [weak self] collectionView, indexPath, attachment in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCollectionViewCell + + if let image = UIImage(data: attachment.imageData) { + cell.configure(with: image, attachmentId: attachment.id) { [weak self] attachmentId in + self?.imageBarDelegate?.inputBoxImageBar(self!, didRemoveImageWithId: attachmentId) + } + } + + return cell + } + } + + private func applySnapshot() { + var snapshot = NSDiffableDataSourceSnapshot< + Section, + ImageAttachment + >() + snapshot.appendSections([.main]) + snapshot.appendItems(attachments) + myDataSource.apply(snapshot, animatingDifferences: true) + } +} + +// MARK: - UICollectionViewDelegate + +extension ImageAttachmentBar: UICollectionViewDelegate {} + +// MARK: - Preview + +#if canImport(SwiftUI) && DEBUG + import SwiftUI + + struct InputBoxImageBar_Previews: PreviewProvider { + static var previews: some View { + UIViewPreview { + let imageBar = ImageAttachmentBar() + + let mockAttachments = [ + createMockImageAttachment(color: .red), + createMockImageAttachment(color: .blue), + createMockImageAttachment(color: .green), + createMockImageAttachment(color: .orange), + createMockImageAttachment(color: .purple), + ] + + imageBar.updateImageBarContent(mockAttachments) + return imageBar + } + .previewLayout(.fixed(width: 400, height: 100)) + .previewDisplayName("Image Bar with Multiple Images") + + UIViewPreview { + let imageBar = ImageAttachmentBar() + + let singleAttachment = [createMockImageAttachment(color: .systemBlue)] + imageBar.updateImageBarContent(singleAttachment) + return imageBar + } + .previewLayout(.fixed(width: 400, height: 100)) + .previewDisplayName("Image Bar with Single Image") + + UIViewPreview { + let imageBar = ImageAttachmentBar() + imageBar.updateImageBarContent([]) + return imageBar + } + .previewLayout(.fixed(width: 400, height: 100)) + .previewDisplayName("Empty Image Bar") + } + + private static func createMockImageAttachment(color: UIColor) -> ImageAttachment { + let size = CGSize(width: 100, height: 100) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { context in + color.setFill() + context.fill(CGRect(origin: .zero, size: size)) + + UIColor.white.withAlphaComponent(0.3).setFill() + let circleRect = CGRect(x: 25, y: 25, width: 50, height: 50) + context.cgContext.fillEllipse(in: circleRect) + } + + return ImageAttachment(image: image) + } + } +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageCollectionViewCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageCollectionViewCell.swift new file mode 100644 index 000000000..486e10a5a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageCollectionViewCell.swift @@ -0,0 +1,61 @@ +// +// ImageCollectionViewCell.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import UIKit + +// MARK: - ImageCollectionViewCell + +class ImageCollectionViewCell: UICollectionViewCell { + private var attachmentId: UUID? + private var onRemove: ((UUID) -> Void)? + + private lazy var imageView = UIImageView().then { + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + $0.layer.cornerRadius = 12 + $0.layer.cornerCurve = .continuous + $0.backgroundColor = .systemGray6 + } + + private lazy var removeButton = DeleteButtonView().then { + $0.onTapped = { [weak self] in + if let attachmentId = self?.attachmentId { + self?.onRemove?(attachmentId) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + contentView.addSubview(imageView) + contentView.addSubview(removeButton) + + imageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + removeButton.snp.makeConstraints { make in + make.top.trailing.equalToSuperview().inset(6) + make.size.equalTo(18) + } + } + + func configure(with image: UIImage, attachmentId: UUID, onRemove: @escaping (UUID) -> Void) { + imageView.image = image + self.attachmentId = attachmentId + self.onRemove = onRemove + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift index 3b9745259..b8296e1ac 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift @@ -43,7 +43,7 @@ class InputBox: UIView { $0.delegate = self } - lazy var imageBar = InputBoxImageBar().then { + lazy var imageBar = ImageAttachmentBar().then { $0.imageBarDelegate = self } @@ -57,6 +57,10 @@ class InputBox: UIView { $0.addArrangedSubview(functionBar) } + lazy var fileAttachmentHeader = FileAttachmentHeaderView().then { + $0.delegate = self + } + var textViewHeightConstraint: Constraint? let minTextViewHeight: CGFloat = 22 let maxTextViewHeight: CGFloat = 100 @@ -74,13 +78,15 @@ class InputBox: UIView { super.init(frame: frame) backgroundColor = .clear + addSubview(fileAttachmentHeader) addSubview(containerView) containerView.addSubview(mainStackView) containerView.addSubview(placeholderLabel) imageBar.isHidden = true containerView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(16) + make.left.bottom.right.equalToSuperview().inset(16) + make.top.greaterThanOrEqualToSuperview().offset(16) } mainStackView.snp.makeConstraints { make in @@ -100,6 +106,11 @@ class InputBox: UIView { make.top.equalTo(textView) } + // for initial status + fileAttachmentHeader.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(32).priority(.high) + } + setupBindings() updatePlaceholderVisibility() updateColors() @@ -118,7 +129,6 @@ class InputBox: UIView { } func setupBindings() { - // 绑定 ViewModel 到 UI viewModel.$inputText .removeDuplicates() .sink { [weak self] text in @@ -158,8 +168,9 @@ class InputBox: UIView { } .store(in: &cancellables) - viewModel.$hasAttachments - .dropFirst() // for view setup + viewModel.$imageAttachments + .dropFirst() // for view setup to remove animation + .map { !$0.isEmpty /* -> hasAttachments */ } .removeDuplicates() .sink { [weak self] hasAttachments in performWithAnimation { @@ -169,12 +180,20 @@ class InputBox: UIView { } .store(in: &cancellables) - viewModel.$attachments + viewModel.$imageAttachments .removeDuplicates() .sink { [weak self] attachments in self?.updateImageBarContent(attachments) } .store(in: &cancellables) + + Publishers.CombineLatest(viewModel.$fileAttachments, viewModel.$documentAttachments) + .dropFirst() // for view setup to remove animation + .removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 } + .sink { [weak self] fileAttachments, documentAttachments in + self?.updateFileAttachmentHeader(fileCount: fileAttachments.count, documentCount: documentAttachments.count) + } + .store(in: &cancellables) } func updateTextViewHeight() { @@ -203,10 +222,34 @@ class InputBox: UIView { imageBar.isHidden = !hasAttachments } - func updateImageBarContent(_ attachments: [InputAttachment]) { + func updateImageBarContent(_ attachments: [ImageAttachment]) { imageBar.updateImageBarContent(attachments) } + func updateFileAttachmentHeader(fileCount: Int, documentCount: Int) { + let hasAttachments = fileCount > 0 || documentCount > 0 + + fileAttachmentHeader.snp.remakeConstraints { make in + if hasAttachments { + make.leading.trailing.equalToSuperview().inset(32) + make.bottom.equalTo(self.containerView.snp.top).offset(8) + make.top.equalToSuperview().offset(8) + } else { + make.edges.equalToSuperview().inset(32).priority(.high) + } + } + + performWithAnimation { + self.fileAttachmentHeader.isHidden = !hasAttachments + if hasAttachments { + self.fileAttachmentHeader.updateContent(attachmentCount: fileCount, docsCount: documentCount) + self.fileAttachmentHeader.setIconImage(UIImage(systemName: "doc")) + } + self.layoutIfNeeded() + self.superview?.layoutIfNeeded() + } + } + func updateColors() { containerView.layer.borderColor = UIColor.affineLayerBorder.cgColor } @@ -214,33 +257,36 @@ class InputBox: UIView { // MARK: - Public Methods public func addImageAttachment(_ image: UIImage) { - guard let imageData = image.jpegData(compressionQuality: 0.8) else { return } - - let attachment = InputAttachment( - type: .image, - data: imageData, - name: "image.jpg", - size: Int64(imageData.count) - ) + let attachment = ImageAttachment(image: image) performWithAnimation { [self] in - viewModel.addAttachment(attachment) + viewModel.addImageAttachment(attachment) layoutIfNeeded() } } - public func addFileAttachment(_ url: URL) { - guard let fileData = try? Data(contentsOf: url) else { return } + public func addFileAttachment(_ url: URL) throws { + // check less then 15mb + let fileSizeLimit: Int64 = 15 * 1024 * 1024 // 15 MB + let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) + guard let fileSize = fileAttributes[.size] as? Int64, fileSize <= fileSizeLimit else { + throw NSError( + domain: "FileAttachmentErrorDomain", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "File size exceeds 15 MB limit."] + ) + } + let fileData = try Data(contentsOf: url) - let attachment = InputAttachment( - type: .file, + let attachment = FileAttachment( data: fileData, + url: url, name: url.lastPathComponent, size: Int64(fileData.count) ) performWithAnimation { [self] in - viewModel.addAttachment(attachment) + viewModel.addFileAttachment(attachment) layoutIfNeeded() } } @@ -249,39 +295,3 @@ class InputBox: UIView { viewModel.prepareSendData() } } - -// MARK: - InputBoxFunctionBarDelegate - -extension InputBox: InputBoxFunctionBarDelegate { - func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectTakePhoto(self) - } - - func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectPhotoLibrary(self) - } - - func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectAttachFiles(self) - } - - func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectEmbedDocs(self) - } - - func functionBarDidTapTool(_: InputBoxFunctionBar) { - viewModel.toggleTool() - } - - func functionBarDidTapNetwork(_: InputBoxFunctionBar) { - viewModel.toggleNetwork() - } - - func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) { - viewModel.toggleDeepThinking() - } - - func functionBarDidTapSend(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSend(self) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift index b89494913..85177d3f1 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift @@ -5,6 +5,7 @@ // Created by 秋星桥 on 6/18/25. // +import SwifterSwift import UIKit protocol InputBoxDelegate: AnyObject { @@ -16,15 +17,76 @@ protocol InputBoxDelegate: AnyObject { func inputBoxTextDidChange(_ text: String) } -extension InputBox: InputBoxImageBarDelegate { - func inputBoxImageBar(_: InputBoxImageBar, didRemoveImageWithId id: InputAttachment.ID) { +extension InputBox: ImageAttachmentBarDelegate { + func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) { performWithAnimation { [self] in - viewModel.removeAttachment(withId: id) + viewModel.removeImageAttachment(withId: id) layoutIfNeeded() } } } +extension InputBox: FileAttachmentHeaderViewDelegate { + func headerViewDidPickMore(_: FileAttachmentHeaderView) { + delegate?.inputBoxDidSelectAttachFiles(self) + } + + func headerViewDidTapManagement(_: FileAttachmentHeaderView) { + let controller = AttachmentManagementController(delegate: self) + controller.set(fileAttachments: viewModel.fileAttachments) + controller.set(documentAttachments: viewModel.documentAttachments) + parentViewController?.present(controller, animated: true) + } +} + +extension InputBox: AttachmentManagementControllerDelegate { + func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) { + viewModel.removeFileAttachment(withId: attachment.id) + controller.set(fileAttachments: viewModel.fileAttachments) + layoutIfNeeded() + } + + func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) { + viewModel.removeDocumentAttachment(withId: attachment.id) + controller.set(documentAttachments: viewModel.documentAttachments) + layoutIfNeeded() + } +} + +extension InputBox: InputBoxFunctionBarDelegate { + func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectTakePhoto(self) + } + + func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectPhotoLibrary(self) + } + + func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectAttachFiles(self) + } + + func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectEmbedDocs(self) + } + + func functionBarDidTapTool(_: InputBoxFunctionBar) { + viewModel.toggleTool() + } + + func functionBarDidTapNetwork(_: InputBoxFunctionBar) { + viewModel.toggleNetwork() + } + + func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) { + viewModel.toggleDeepThinking() + } + + func functionBarDidTapSend(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSend(self) + } +} + extension InputBox: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { viewModel.updateText(textView.text ?? "") diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift index db3771a26..1e97df556 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift @@ -156,7 +156,7 @@ class InputBoxFunctionBar: UIView { } let attachFilesAction = UIAction( - title: "Attach Files (pdf, txt, csv)", + title: "Attach Files (.pdf, .txt, .csv)", image: UIImage.affineUpload ) { [weak self] _ in guard let self else { return } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift deleted file mode 100644 index 8b1cdf357..000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// InputBoxImageBar.swift -// Intelligents -// -// Created by 秋星桥 on 6/18/25. -// - -import SnapKit -import Then -import UIKit - -protocol InputBoxImageBarDelegate: AnyObject { - func inputBoxImageBar(_ imageBar: InputBoxImageBar, didRemoveImageWithId id: UUID) -} - -private class AttachmentViewModel { - let attachment: InputAttachment - let imageCell: InputBoxImageBar.ImageCell - - init(attachment: InputAttachment, imageCell: InputBoxImageBar.ImageCell) { - self.attachment = attachment - self.imageCell = imageCell - } -} - -class InputBoxImageBar: UIScrollView { - weak var imageBarDelegate: InputBoxImageBarDelegate? - - private var attachmentViewModels: [AttachmentViewModel] = [] - private let cellSpacing: CGFloat = 8 - private let constantHeight: CGFloat = 80 - - override init(frame: CGRect = .zero) { - super.init(frame: frame) - showsHorizontalScrollIndicator = false - showsVerticalScrollIndicator = false - - snp.makeConstraints { make in - make.height.equalTo(constantHeight) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - func updateImageBarContent(_ attachments: [InputAttachment]) { - let currentIds = Set(attachmentViewModels.map(\.attachment.id)) - let imageAttachments = attachments.filter { $0.type == .image } - let newIds = Set(imageAttachments.map(\.id)) - - // 移除不再存在的附件 - let idsToRemove = currentIds.subtracting(newIds) - for id in idsToRemove { - if let index = attachmentViewModels.firstIndex(where: { $0.attachment.id == id }) { - let viewModel = attachmentViewModels.remove(at: index) - viewModel.imageCell.removeFromSuperview() - } - } - - // 添加新的附件 - let idsToAdd = newIds.subtracting(currentIds) - var initialXOffset = attachmentViewModels.reduce(0) { $0 + $1.imageCell.frame.width + cellSpacing } - for attachment in imageAttachments { - if idsToAdd.contains(attachment.id), - let data = attachment.data, - let image = UIImage(data: data) - { - let imageCell = ImageCell( - // for animation to work - frame: .init(x: initialXOffset, y: 0, width: constantHeight, height: constantHeight), - image: image, - attachmentId: attachment.id - ) - initialXOffset += constantHeight + cellSpacing - imageCell.onRemove = { [weak self] cell in - self?.removeImageCell(cell) - } - imageCell.alpha = 0 - DispatchQueue.main.async { - performWithAnimation { imageCell.alpha = 1 } - } - - let viewModel = AttachmentViewModel(attachment: attachment, imageCell: imageCell) - attachmentViewModels.append(viewModel) - addSubview(imageCell) - } - } - - layoutImageCells() - } - - func removeImageCell(_ cell: ImageCell) { - if let index = attachmentViewModels.firstIndex(where: { $0.imageCell === cell }) { - let viewModel = attachmentViewModels.remove(at: index) - viewModel.imageCell.removeFromSuperviewWithExplodeEffect() - imageBarDelegate?.inputBoxImageBar(self, didRemoveImageWithId: cell.attachmentId) - layoutImageCells() - } - } - - func clear() { - for viewModel in attachmentViewModels { - viewModel.imageCell.removeFromSuperview() - } - attachmentViewModels.removeAll() - contentSize = .zero - } - - private func layoutImageCells() { - var xOffset: CGFloat = 0 - - for viewModel in attachmentViewModels { - viewModel.imageCell.frame = CGRect(x: xOffset, y: 0, width: constantHeight, height: constantHeight) - xOffset += constantHeight + cellSpacing - } - - // Update content size - let totalWidth = max(0, xOffset - cellSpacing) - contentSize = CGSize(width: totalWidth, height: constantHeight) - } -} - -extension InputBoxImageBar { - class ImageCell: UIView { - let attachmentId: UUID - var onRemove: ((ImageCell) -> Void)? - - private lazy var imageView = UIImageView(frame: bounds).then { - $0.contentMode = .scaleAspectFill - $0.clipsToBounds = true - $0.layer.cornerRadius = 12 - $0.layer.cornerCurve = .continuous - $0.backgroundColor = .systemGray6 - } - - private lazy var removeButton = DeleteButtonView(frame: removeButtonFrame).then { - $0.onTapped = { [weak self] in - self?.removeButtonTapped() - } - } - - init(frame: CGRect, image: UIImage, attachmentId: UUID) { - self.attachmentId = attachmentId - super.init(frame: frame) - addSubview(imageView) - addSubview(removeButton) - imageView.image = image - } - - var removeButtonFrame: CGRect { - let buttonSize: CGFloat = 18 - let buttonInset: CGFloat = 6 - return CGRect( - x: bounds.width - buttonSize - buttonInset, - y: buttonInset, - width: buttonSize, - height: buttonSize - ) - } - - override func layoutSubviews() { - super.layoutSubviews() - - imageView.frame = bounds - - removeButton.frame = removeButtonFrame - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - @objc private func removeButtonTapped() { - onRemove?(self) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift deleted file mode 100644 index 9a3a24912..000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// InputBoxViewModel.swift -// Intelligents -// -// Created by AI Assistant on 6/17/25. -// - -import Combine -import Foundation - -// MARK: - Data Models - -public struct InputAttachment: Identifiable, Equatable, Hashable, Codable { - public var id: UUID = .init() - public var type: AttachmentType - public var data: Data? - public var url: URL? - public var name: String - public var size: Int64 - - public enum AttachmentType: String, Equatable, Hashable, Codable { - case image - case document - case file - } - - public init( - type: AttachmentType, - data: Data? = nil, - url: URL? = nil, - name: String, - size: Int64 = 0 - ) { - self.type = type - self.data = data - self.url = url - self.name = name - self.size = size - } -} - -public struct InputBoxData { - public var text: String - public var attachments: [InputAttachment] - public var isToolEnabled: Bool - public var isNetworkEnabled: Bool - public var isDeepThinkingEnabled: Bool - - public init( - text: String, - attachments: [InputAttachment], - isToolEnabled: Bool, - isNetworkEnabled: Bool, - isDeepThinkingEnabled: Bool - ) { - self.text = text - self.attachments = attachments - self.isToolEnabled = isToolEnabled - self.isNetworkEnabled = isNetworkEnabled - self.isDeepThinkingEnabled = isDeepThinkingEnabled - } -} - -// MARK: - View Model - -public class InputBoxViewModel: ObservableObject { - // MARK: - Published Properties - - @Published public var inputText: String = "" - @Published public var isToolEnabled: Bool = false - @Published public var isNetworkEnabled: Bool = false - @Published public var isDeepThinkingEnabled: Bool = false - @Published public var hasAttachments: Bool = false - @Published public var attachments: [InputAttachment] = [] - @Published public var canSend: Bool = false - - // MARK: - Private Properties - - private var cancellables = Set() - - // MARK: - Initialization - - public init() { - setupBindings() - } - - // MARK: - Private Methods - - private func setupBindings() { - // 监听文本变化,自动更新发送按钮状态 - $inputText - .map { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - .assign(to: \.canSend, on: self) - .store(in: &cancellables) - - // 监听附件变化 - $attachments - .map { !$0.isEmpty } - .assign(to: \.hasAttachments, on: self) - .store(in: &cancellables) - } -} - -// MARK: - Text Management - -public extension InputBoxViewModel { - func updateText(_ text: String) { - inputText = text - } -} - -// MARK: - Feature Toggles - -public extension InputBoxViewModel { - func toggleTool() { - isToolEnabled.toggle() - } - - func toggleNetwork() { - isNetworkEnabled.toggle() - } - - func toggleDeepThinking() { - isDeepThinkingEnabled.toggle() - } -} - -// MARK: - Attachment Management - -public extension InputBoxViewModel { - func addAttachment(_ attachment: InputAttachment) { - attachments.append(attachment) - } - - func removeAttachment(withId id: UUID) { - attachments.removeAll { $0.id == id } - } - - func clearAttachments() { - attachments.removeAll() - } -} - -// MARK: - Send Management - -public extension InputBoxViewModel { - func prepareSendData() -> InputBoxData { - InputBoxData( - text: inputText.trimmingCharacters(in: .whitespacesAndNewlines), - attachments: attachments, - isToolEnabled: isToolEnabled, - isNetworkEnabled: isNetworkEnabled, - isDeepThinkingEnabled: isDeepThinkingEnabled - ) - } - - func resetInput() { - inputText = "" - attachments.removeAll() - isToolEnabled = false - isNetworkEnabled = false - isDeepThinkingEnabled = false - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift new file mode 100644 index 000000000..1f94bd79a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift @@ -0,0 +1,15 @@ +// +// DocumentAttachment.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import Foundation + +public struct DocumentAttachment: Identifiable, Equatable, Hashable, Codable { + public var id: UUID = .init() + public var title: String = "" + public var workspaceID: String = "" + public var documentID: String = "" +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/FileAttachment.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/FileAttachment.swift new file mode 100644 index 000000000..72c8415a8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/FileAttachment.swift @@ -0,0 +1,28 @@ +// +// FileAttachment.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import Foundation + +public struct FileAttachment: Identifiable, Equatable, Hashable, Codable { + public var id: UUID = .init() + public var data: Data? + public var url: URL + public var name: String + public var size: Int64 + + public init( + data: Data? = nil, + url: URL, + name: String, + size: Int64 = 0 + ) { + self.data = data + self.url = url + self.name = name + self.size = size + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/ImageAttachment.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/ImageAttachment.swift new file mode 100644 index 000000000..1a45095a9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/ImageAttachment.swift @@ -0,0 +1,17 @@ +// +// ImageAttachment.swift +// Intelligents +// +// Created by 秋星桥 on 6/24/25. +// + +import UIKit + +public struct ImageAttachment: Identifiable, Equatable, Hashable, Codable { + public var id: UUID = .init() + public var imageData: Data + + public init(image: UIImage) { + imageData = image.jpegData(compressionQuality: 0.5) ?? Data() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/InputBoxViewModel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/InputBoxViewModel.swift new file mode 100644 index 000000000..fccd1c418 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/InputBoxViewModel.swift @@ -0,0 +1,159 @@ +// +// InputBoxViewModel.swift +// Intelligents +// +// Created by AI Assistant on 6/17/25. +// + +import Combine +import Foundation + +// MARK: - Data Models + +public struct InputBoxData { + public var text: String + public var imageAttachments: [ImageAttachment] + public var fileAttachments: [FileAttachment] = [] + public var documentAttachments: [DocumentAttachment] = [] + public var isToolEnabled: Bool + public var isNetworkEnabled: Bool + public var isDeepThinkingEnabled: Bool + + public init(text: String, imageAttachments: [ImageAttachment], fileAttachments: [FileAttachment], documentAttachments: [DocumentAttachment], isToolEnabled: Bool, isNetworkEnabled: Bool, isDeepThinkingEnabled: Bool) { + self.text = text + self.imageAttachments = imageAttachments + self.fileAttachments = fileAttachments + self.documentAttachments = documentAttachments + self.isToolEnabled = isToolEnabled + self.isNetworkEnabled = isNetworkEnabled + self.isDeepThinkingEnabled = isDeepThinkingEnabled + } +} + +// MARK: - View Model + +public class InputBoxViewModel: ObservableObject { + // MARK: - Published Properties + + @Published public var inputText: String = "" + @Published public var isToolEnabled: Bool = false + @Published public var isNetworkEnabled: Bool = false + @Published public var isDeepThinkingEnabled: Bool = false + @Published public var imageAttachments: [ImageAttachment] = [] + @Published public var fileAttachments: [FileAttachment] = [] + @Published public var documentAttachments: [DocumentAttachment] = [] + @Published public var canSend: Bool = false + + // MARK: - Private Properties + + private var cancellables = Set() + + // MARK: - Initialization + + public init() { + setupBindings() + } + + // MARK: - Private Methods + + private func setupBindings() { + Publishers.CombineLatest4($inputText, $imageAttachments, $fileAttachments, $documentAttachments) + .map { text, images, files, docs in + let hasText = !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + let hasAnyAttachments = !images.isEmpty || !files.isEmpty || !docs.isEmpty + return hasText || hasAnyAttachments + } + .assign(to: \.canSend, on: self) + .store(in: &cancellables) + } +} + +// MARK: - Text Management + +public extension InputBoxViewModel { + func updateText(_ text: String) { + inputText = text + } +} + +// MARK: - Feature Toggles + +public extension InputBoxViewModel { + func toggleTool() { + isToolEnabled.toggle() + } + + func toggleNetwork() { + isNetworkEnabled.toggle() + } + + func toggleDeepThinking() { + isDeepThinkingEnabled.toggle() + } +} + +// MARK: - Attachment Management + +public extension InputBoxViewModel { + func addImageAttachment(_ attachment: ImageAttachment) { + imageAttachments.append(attachment) + } + + func removeImageAttachment(withId id: UUID) { + imageAttachments.removeAll { $0.id == id } + } + + func clearImageAttachments() { + imageAttachments.removeAll() + } + + func addFileAttachment(_ attachment: FileAttachment) { + fileAttachments.append(attachment) + } + + func removeFileAttachment(withId id: UUID) { + fileAttachments.removeAll { $0.id == id } + } + + func clearFileAttachments() { + fileAttachments.removeAll() + } + + func addDocumentAttachment(_ attachment: DocumentAttachment) { + documentAttachments.append(attachment) + } + + func removeDocumentAttachment(withId id: UUID) { + documentAttachments.removeAll { $0.id == id } + } + + func clearDocumentAttachments() { + documentAttachments.removeAll() + } +} + +// MARK: - Send Management + +public extension InputBoxViewModel { + func prepareSendData() -> InputBoxData { + InputBoxData( + text: inputText.trimmingCharacters(in: .whitespacesAndNewlines), + imageAttachments: imageAttachments, + fileAttachments: fileAttachments, + documentAttachments: documentAttachments, + isToolEnabled: isToolEnabled, + isNetworkEnabled: isNetworkEnabled, + isDeepThinkingEnabled: isDeepThinkingEnabled + ) + } + + func resetInput() { + inputText = "" + imageAttachments.removeAll() + fileAttachments.removeAll() + documentAttachments.removeAll() + isToolEnabled = false + isNetworkEnabled = false + isDeepThinkingEnabled = false + } +}