chroe: input box view model

This commit is contained in:
Lakr
2025-06-18 18:52:44 +08:00
parent 8065fa4bf4
commit ea1e7076d7
6 changed files with 265 additions and 21 deletions
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
@@ -77,6 +77,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
C45499AB2D140B5000E21978 /* NBStore */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = NBStore;
sourceTree = "<group>";
};
@@ -324,13 +326,9 @@
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
@@ -27,7 +27,7 @@ let package = Package(
.product(name: "Apollo", package: "apollo-ios"),
.product(name: "OrderedCollections", package: "swift-collections"),
], resources: [
.process("Interface/View/InputBox/InputBox.xcassets")
.process("Interface/View/InputBox/InputBox.xcassets"),
]),
]
)
@@ -1,3 +1,4 @@
import Combine
import SnapKit
import Then
import UIKit
@@ -13,6 +14,11 @@ class MainViewController: UIViewController {
$0.delegate = self
}
// MARK: - Properties
private var cancellables = Set<AnyCancellable>()
private let intelligentContext = IntelligentContext.shared
// MARK: - Lifecycle
override func viewDidLoad() {
@@ -72,27 +78,30 @@ extension MainViewController: MainHeaderViewDelegate {
extension MainViewController: InputBoxDelegate {
func inputBoxDidTapAddAttachment() {
// TODO:
print("Add attachment tapped")
}
func inputBoxDidTapTool() {
print("Tool toggled: \(inputBox.viewModel.isToolEnabled)")
}
func inputBoxDidTapNetwork() {
print("Network toggled: \(inputBox.viewModel.isNetworkEnabled)")
}
func inputBoxDidTapDeepThinking() {
print("Deep thinking toggled: \(inputBox.viewModel.isDeepThinkingEnabled)")
}
func inputBoxDidTapSend() {
func inputBoxDidTapSend(data: InputBoxData) {
//
guard !data.text.isEmpty else { return }
print("[*] send tapped with text: \(data.text)")
}
func inputBoxTextDidChange(_ text: String) {
//
print("Text changed: \(text)")
}
}
@@ -1,3 +1,4 @@
import Combine
import SnapKit
import Then
import UIKit
@@ -7,13 +8,18 @@ protocol InputBoxDelegate: AnyObject {
func inputBoxDidTapTool()
func inputBoxDidTapNetwork()
func inputBoxDidTapDeepThinking()
func inputBoxDidTapSend()
func inputBoxDidTapSend(data: InputBoxData)
func inputBoxTextDidChange(_ text: String)
}
class InputBox: UIView {
weak var delegate: InputBoxDelegate?
// MARK: - ViewModel
public let viewModel = InputBoxViewModel()
private var cancellables = Set<AnyCancellable>()
private lazy var containerView = UIView().then {
$0.backgroundColor = .systemBackground
$0.layer.cornerRadius = 12
@@ -136,6 +142,7 @@ class InputBox: UIView {
super.init(frame: .zero)
setupViews()
setupConstraints()
setupBindings()
updatePlaceholderVisibility()
}
@@ -151,6 +158,56 @@ class InputBox: UIView {
containerView.addSubview(placeholderLabel)
}
private func setupBindings() {
// ViewModel UI
viewModel.$inputText
.sink { [weak self] text in
if self?.textView.text != text {
self?.textView.text = text
self?.updatePlaceholderVisibility()
self?.updateTextViewHeight()
}
}
.store(in: &cancellables)
viewModel.$isToolEnabled
.sink { [weak self] enabled in
self?.toolButton.isSelected = enabled
self?.toolButton.tintColor = enabled ? .systemBlue : .secondaryLabel
}
.store(in: &cancellables)
viewModel.$isNetworkEnabled
.sink { [weak self] enabled in
self?.webButton.isSelected = enabled
self?.webButton.tintColor = enabled ? .systemBlue : .secondaryLabel
}
.store(in: &cancellables)
viewModel.$isDeepThinkingEnabled
.sink { [weak self] enabled in
self?.reactButton.isSelected = enabled
self?.reactButton.tintColor = enabled ? .systemBlue : .secondaryLabel
}
.store(in: &cancellables)
viewModel.$canSend
.sink { [weak self] canSend in
self?.sendButton.isEnabled = canSend
self?.sendButton.alpha = canSend ? 1.0 : 0.5
}
.store(in: &cancellables)
viewModel.$isSending
.sink { [weak self] isSending in
self?.sendButton.isEnabled = !isSending
if isSending {
// TODO:
}
}
.store(in: &cancellables)
}
private func setupConstraints() {
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
@@ -212,19 +269,23 @@ class InputBox: UIView {
}
@objc private func toolButtonTapped() {
viewModel.toggleTool()
delegate?.inputBoxDidTapTool()
}
@objc private func webButtonTapped() {
viewModel.toggleNetwork()
delegate?.inputBoxDidTapNetwork()
}
@objc private func reactButtonTapped() {
viewModel.toggleDeepThinking()
delegate?.inputBoxDidTapDeepThinking()
}
@objc private func sendButtonTapped() {
delegate?.inputBoxDidTapSend()
let data = viewModel.prepareSendData()
delegate?.inputBoxDidTapSend(data: data)
}
}
@@ -234,6 +295,7 @@ extension InputBox: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updatePlaceholderVisibility()
updateTextViewHeight()
viewModel.updateText(textView.text)
delegate?.inputBoxTextDidChange(textView.text)
}
}
@@ -0,0 +1,172 @@
//
// InputBoxViewModel.swift
// Intelligents
//
// Created by AI Assistant on 6/17/25.
//
import Combine
import Foundation
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 isSending: 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: - Public Properties
@Published public var canSend: Bool = false
// MARK: - Public Methods
public func updateText(_ text: String) {
inputText = text
}
public func toggleTool() {
isToolEnabled.toggle()
}
public func toggleNetwork() {
isNetworkEnabled.toggle()
}
public func toggleDeepThinking() {
isDeepThinkingEnabled.toggle()
}
public func addAttachment(_ attachment: InputAttachment) {
attachments.append(attachment)
}
public func removeAttachment(at index: Int) {
guard index < attachments.count else { return }
attachments.remove(at: index)
}
public func clearAttachments() {
attachments.removeAll()
}
public func prepareSendData() -> InputBoxData {
InputBoxData(
text: inputText.trimmingCharacters(in: .whitespacesAndNewlines),
attachments: attachments,
isToolEnabled: isToolEnabled,
isNetworkEnabled: isNetworkEnabled,
isDeepThinkingEnabled: isDeepThinkingEnabled
)
}
public func resetInput() {
inputText = ""
attachments.removeAll()
isToolEnabled = false
isNetworkEnabled = false
isDeepThinkingEnabled = false
isSending = false
}
public func setSending(_ sending: Bool) {
isSending = sending
}
}
// MARK: - Supporting Types
public struct InputBoxData {
public let text: String
public let attachments: [InputAttachment]
public let isToolEnabled: Bool
public let isNetworkEnabled: Bool
public let 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
}
}
public struct InputAttachment {
public let id: String
public let type: AttachmentType
public let data: Data?
public let url: URL?
public let name: String
public let size: Int64
public init(
id: String = UUID().uuidString,
type: AttachmentType,
data: Data? = nil,
url: URL? = nil,
name: String,
size: Int64 = 0
) {
self.id = id
self.type = type
self.data = data
self.url = url
self.name = name
self.size = size
}
}
public enum AttachmentType {
case image
case document
case video
case audio
case other(String)
public var displayName: String {
switch self {
case .image: "Image"
case .document: "Document"
case .video: "Video"
case .audio: "Audio"
case let .other(type): type
}
}
}
@@ -5,6 +5,7 @@
// Created by on 6/17/25.
//
import Combine
import Foundation
import WebKit
@@ -21,4 +22,6 @@ public class IntelligentContext {
// TODO: if needed
completion()
}
// MARK: - Input Processing
}