mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
chore: made attachment header & management sheet (#12922)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -10,7 +10,7 @@ let package = Package(
|
|||||||
.iOS(.v16),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "Intelligents", targets: ["Intelligents"]),
|
.library(name: "Intelligents", type: .dynamic, targets: ["Intelligents"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(path: "../AffineGraphQL"),
|
.package(path: "../AffineGraphQL"),
|
||||||
@@ -29,8 +29,8 @@ let package = Package(
|
|||||||
.product(name: "Apollo", package: "apollo-ios"),
|
.product(name: "Apollo", package: "apollo-ios"),
|
||||||
.product(name: "OrderedCollections", package: "swift-collections"),
|
.product(name: "OrderedCollections", package: "swift-collections"),
|
||||||
], resources: [
|
], resources: [
|
||||||
.process("Resources/main.metal"),
|
|
||||||
.process("Interface/View/InputBox/InputBox.xcassets"),
|
.process("Interface/View/InputBox/InputBox.xcassets"),
|
||||||
|
.process("Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets"),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ViewController: UIViewController>: 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<View: UIView>: 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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Page.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AiFileClip attachment icon.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AiFileClip attachment icon.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AiFileClip attachment icon.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AiFileClip attachment icon.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Section, Item>()
|
||||||
|
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
|
||||||
@@ -114,9 +114,15 @@ extension MainViewController: UIDocumentPickerDelegate {
|
|||||||
try FileManager.default.copyItem(at: url, to: tempURL)
|
try FileManager.default.copyItem(at: url, to: tempURL)
|
||||||
|
|
||||||
// Add file attachment using the temporary URL
|
// Add file attachment using the temporary URL
|
||||||
inputBox.addFileAttachment(tempURL)
|
try inputBox.addFileAttachment(tempURL)
|
||||||
} catch {
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ class InputBox: UIView {
|
|||||||
$0.delegate = self
|
$0.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy var imageBar = InputBoxImageBar().then {
|
lazy var imageBar = ImageAttachmentBar().then {
|
||||||
$0.imageBarDelegate = self
|
$0.imageBarDelegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,10 @@ class InputBox: UIView {
|
|||||||
$0.addArrangedSubview(functionBar)
|
$0.addArrangedSubview(functionBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy var fileAttachmentHeader = FileAttachmentHeaderView().then {
|
||||||
|
$0.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
var textViewHeightConstraint: Constraint?
|
var textViewHeightConstraint: Constraint?
|
||||||
let minTextViewHeight: CGFloat = 22
|
let minTextViewHeight: CGFloat = 22
|
||||||
let maxTextViewHeight: CGFloat = 100
|
let maxTextViewHeight: CGFloat = 100
|
||||||
@@ -74,13 +78,15 @@ class InputBox: UIView {
|
|||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
addSubview(fileAttachmentHeader)
|
||||||
addSubview(containerView)
|
addSubview(containerView)
|
||||||
containerView.addSubview(mainStackView)
|
containerView.addSubview(mainStackView)
|
||||||
containerView.addSubview(placeholderLabel)
|
containerView.addSubview(placeholderLabel)
|
||||||
imageBar.isHidden = true
|
imageBar.isHidden = true
|
||||||
|
|
||||||
containerView.snp.makeConstraints { make in
|
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
|
mainStackView.snp.makeConstraints { make in
|
||||||
@@ -100,6 +106,11 @@ class InputBox: UIView {
|
|||||||
make.top.equalTo(textView)
|
make.top.equalTo(textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for initial status
|
||||||
|
fileAttachmentHeader.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(32).priority(.high)
|
||||||
|
}
|
||||||
|
|
||||||
setupBindings()
|
setupBindings()
|
||||||
updatePlaceholderVisibility()
|
updatePlaceholderVisibility()
|
||||||
updateColors()
|
updateColors()
|
||||||
@@ -118,7 +129,6 @@ class InputBox: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupBindings() {
|
func setupBindings() {
|
||||||
// 绑定 ViewModel 到 UI
|
|
||||||
viewModel.$inputText
|
viewModel.$inputText
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] text in
|
.sink { [weak self] text in
|
||||||
@@ -158,8 +168,9 @@ class InputBox: UIView {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$hasAttachments
|
viewModel.$imageAttachments
|
||||||
.dropFirst() // for view setup
|
.dropFirst() // for view setup to remove animation
|
||||||
|
.map { !$0.isEmpty /* -> hasAttachments */ }
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] hasAttachments in
|
.sink { [weak self] hasAttachments in
|
||||||
performWithAnimation {
|
performWithAnimation {
|
||||||
@@ -169,12 +180,20 @@ class InputBox: UIView {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$attachments
|
viewModel.$imageAttachments
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] attachments in
|
.sink { [weak self] attachments in
|
||||||
self?.updateImageBarContent(attachments)
|
self?.updateImageBarContent(attachments)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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() {
|
func updateTextViewHeight() {
|
||||||
@@ -203,10 +222,34 @@ class InputBox: UIView {
|
|||||||
imageBar.isHidden = !hasAttachments
|
imageBar.isHidden = !hasAttachments
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateImageBarContent(_ attachments: [InputAttachment]) {
|
func updateImageBarContent(_ attachments: [ImageAttachment]) {
|
||||||
imageBar.updateImageBarContent(attachments)
|
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() {
|
func updateColors() {
|
||||||
containerView.layer.borderColor = UIColor.affineLayerBorder.cgColor
|
containerView.layer.borderColor = UIColor.affineLayerBorder.cgColor
|
||||||
}
|
}
|
||||||
@@ -214,33 +257,36 @@ class InputBox: UIView {
|
|||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
|
|
||||||
public func addImageAttachment(_ image: UIImage) {
|
public func addImageAttachment(_ image: UIImage) {
|
||||||
guard let imageData = image.jpegData(compressionQuality: 0.8) else { return }
|
let attachment = ImageAttachment(image: image)
|
||||||
|
|
||||||
let attachment = InputAttachment(
|
|
||||||
type: .image,
|
|
||||||
data: imageData,
|
|
||||||
name: "image.jpg",
|
|
||||||
size: Int64(imageData.count)
|
|
||||||
)
|
|
||||||
|
|
||||||
performWithAnimation { [self] in
|
performWithAnimation { [self] in
|
||||||
viewModel.addAttachment(attachment)
|
viewModel.addImageAttachment(attachment)
|
||||||
layoutIfNeeded()
|
layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addFileAttachment(_ url: URL) {
|
public func addFileAttachment(_ url: URL) throws {
|
||||||
guard let fileData = try? Data(contentsOf: url) else { return }
|
// 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(
|
let attachment = FileAttachment(
|
||||||
type: .file,
|
|
||||||
data: fileData,
|
data: fileData,
|
||||||
|
url: url,
|
||||||
name: url.lastPathComponent,
|
name: url.lastPathComponent,
|
||||||
size: Int64(fileData.count)
|
size: Int64(fileData.count)
|
||||||
)
|
)
|
||||||
|
|
||||||
performWithAnimation { [self] in
|
performWithAnimation { [self] in
|
||||||
viewModel.addAttachment(attachment)
|
viewModel.addFileAttachment(attachment)
|
||||||
layoutIfNeeded()
|
layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,39 +295,3 @@ class InputBox: UIView {
|
|||||||
viewModel.prepareSendData()
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Created by 秋星桥 on 6/18/25.
|
// Created by 秋星桥 on 6/18/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import SwifterSwift
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol InputBoxDelegate: AnyObject {
|
protocol InputBoxDelegate: AnyObject {
|
||||||
@@ -16,15 +17,76 @@ protocol InputBoxDelegate: AnyObject {
|
|||||||
func inputBoxTextDidChange(_ text: String)
|
func inputBoxTextDidChange(_ text: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InputBox: InputBoxImageBarDelegate {
|
extension InputBox: ImageAttachmentBarDelegate {
|
||||||
func inputBoxImageBar(_: InputBoxImageBar, didRemoveImageWithId id: InputAttachment.ID) {
|
func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) {
|
||||||
performWithAnimation { [self] in
|
performWithAnimation { [self] in
|
||||||
viewModel.removeAttachment(withId: id)
|
viewModel.removeImageAttachment(withId: id)
|
||||||
layoutIfNeeded()
|
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 {
|
extension InputBox: UITextViewDelegate {
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
viewModel.updateText(textView.text ?? "")
|
viewModel.updateText(textView.text ?? "")
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class InputBoxFunctionBar: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let attachFilesAction = UIAction(
|
let attachFilesAction = UIAction(
|
||||||
title: "Attach Files (pdf, txt, csv)",
|
title: "Attach Files (.pdf, .txt, .csv)",
|
||||||
image: UIImage.affineUpload
|
image: UIImage.affineUpload
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<AnyCancellable>()
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 = ""
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user