mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 11:30:04 +08:00
chroe: input box view model
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 56;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -77,6 +77,8 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
C45499AB2D140B5000E21978 /* NBStore */ = {
|
C45499AB2D140B5000E21978 /* NBStore */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = NBStore;
|
path = NBStore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -324,13 +326,9 @@
|
|||||||
);
|
);
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
|
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: "Apollo", package: "apollo-ios"),
|
||||||
.product(name: "OrderedCollections", package: "swift-collections"),
|
.product(name: "OrderedCollections", package: "swift-collections"),
|
||||||
], resources: [
|
], resources: [
|
||||||
.process("Interface/View/InputBox/InputBox.xcassets")
|
.process("Interface/View/InputBox/InputBox.xcassets"),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-13
@@ -1,3 +1,4 @@
|
|||||||
|
import Combine
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import Then
|
import Then
|
||||||
import UIKit
|
import UIKit
|
||||||
@@ -13,6 +14,11 @@ class MainViewController: UIViewController {
|
|||||||
$0.delegate = self
|
$0.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let intelligentContext = IntelligentContext.shared
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@@ -72,27 +78,30 @@ extension MainViewController: MainHeaderViewDelegate {
|
|||||||
|
|
||||||
extension MainViewController: InputBoxDelegate {
|
extension MainViewController: InputBoxDelegate {
|
||||||
func inputBoxDidTapAddAttachment() {
|
func inputBoxDidTapAddAttachment() {
|
||||||
|
// TODO: 实现添加附件功能
|
||||||
|
print("Add attachment tapped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputBoxDidTapTool() {
|
func inputBoxDidTapTool() {
|
||||||
|
print("Tool toggled: \(inputBox.viewModel.isToolEnabled)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputBoxDidTapNetwork() {
|
func inputBoxDidTapNetwork() {
|
||||||
|
print("Network toggled: \(inputBox.viewModel.isNetworkEnabled)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputBoxDidTapDeepThinking() {
|
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) {
|
func inputBoxTextDidChange(_ text: String) {
|
||||||
|
// 可以在这里处理文本变化的其他逻辑
|
||||||
|
print("Text changed: \(text)")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import Combine
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import Then
|
import Then
|
||||||
import UIKit
|
import UIKit
|
||||||
@@ -7,13 +8,18 @@ protocol InputBoxDelegate: AnyObject {
|
|||||||
func inputBoxDidTapTool()
|
func inputBoxDidTapTool()
|
||||||
func inputBoxDidTapNetwork()
|
func inputBoxDidTapNetwork()
|
||||||
func inputBoxDidTapDeepThinking()
|
func inputBoxDidTapDeepThinking()
|
||||||
func inputBoxDidTapSend()
|
func inputBoxDidTapSend(data: InputBoxData)
|
||||||
func inputBoxTextDidChange(_ text: String)
|
func inputBoxTextDidChange(_ text: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputBox: UIView {
|
class InputBox: UIView {
|
||||||
weak var delegate: InputBoxDelegate?
|
weak var delegate: InputBoxDelegate?
|
||||||
|
|
||||||
|
// MARK: - ViewModel
|
||||||
|
|
||||||
|
public let viewModel = InputBoxViewModel()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private lazy var containerView = UIView().then {
|
private lazy var containerView = UIView().then {
|
||||||
$0.backgroundColor = .systemBackground
|
$0.backgroundColor = .systemBackground
|
||||||
$0.layer.cornerRadius = 12
|
$0.layer.cornerRadius = 12
|
||||||
@@ -136,6 +142,7 @@ class InputBox: UIView {
|
|||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
setupViews()
|
setupViews()
|
||||||
setupConstraints()
|
setupConstraints()
|
||||||
|
setupBindings()
|
||||||
updatePlaceholderVisibility()
|
updatePlaceholderVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +158,56 @@ class InputBox: UIView {
|
|||||||
containerView.addSubview(placeholderLabel)
|
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() {
|
private func setupConstraints() {
|
||||||
containerView.snp.makeConstraints { make in
|
containerView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(16)
|
make.edges.equalToSuperview().inset(16)
|
||||||
@@ -212,19 +269,23 @@ class InputBox: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func toolButtonTapped() {
|
@objc private func toolButtonTapped() {
|
||||||
|
viewModel.toggleTool()
|
||||||
delegate?.inputBoxDidTapTool()
|
delegate?.inputBoxDidTapTool()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func webButtonTapped() {
|
@objc private func webButtonTapped() {
|
||||||
|
viewModel.toggleNetwork()
|
||||||
delegate?.inputBoxDidTapNetwork()
|
delegate?.inputBoxDidTapNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func reactButtonTapped() {
|
@objc private func reactButtonTapped() {
|
||||||
|
viewModel.toggleDeepThinking()
|
||||||
delegate?.inputBoxDidTapDeepThinking()
|
delegate?.inputBoxDidTapDeepThinking()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sendButtonTapped() {
|
@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) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
updatePlaceholderVisibility()
|
updatePlaceholderVisibility()
|
||||||
updateTextViewHeight()
|
updateTextViewHeight()
|
||||||
|
viewModel.updateText(textView.text)
|
||||||
delegate?.inputBoxTextDidChange(textView.text)
|
delegate?.inputBoxTextDidChange(textView.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+172
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -5,6 +5,7 @@
|
|||||||
// Created by 秋星桥 on 6/17/25.
|
// Created by 秋星桥 on 6/17/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
@@ -21,4 +22,6 @@ public class IntelligentContext {
|
|||||||
// TODO: if needed
|
// TODO: if needed
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Input Processing
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user