mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
chore: input box ux
This commit is contained in:
+9
-3
@@ -35,7 +35,7 @@ class BlurTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate
|
|||||||
|
|
||||||
class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
private let presenting: Bool
|
private let presenting: Bool
|
||||||
private let duration: TimeInterval = 0.5
|
private let duration: TimeInterval = 0.75
|
||||||
|
|
||||||
private let snapshotViewTag = "snapshotView".hashValue
|
private let snapshotViewTag = "snapshotView".hashValue
|
||||||
private let blurViewTag = "blurView".hashValue
|
private let blurViewTag = "blurView".hashValue
|
||||||
@@ -52,8 +52,8 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
|||||||
UIView.animate(
|
UIView.animate(
|
||||||
withDuration: duration,
|
withDuration: duration,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
usingSpringWithDamping: 0.75,
|
usingSpringWithDamping: 0.8,
|
||||||
initialSpringVelocity: 0.75,
|
initialSpringVelocity: 0.8,
|
||||||
options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut],
|
options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut],
|
||||||
animations: animations,
|
animations: animations,
|
||||||
completion: completion
|
completion: completion
|
||||||
@@ -106,11 +106,15 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
|||||||
toView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
|
toView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
|
||||||
containerView.addSubview(toView)
|
containerView.addSubview(toView)
|
||||||
|
|
||||||
|
toView.layoutIfNeeded()
|
||||||
|
|
||||||
performAnimation(animations: {
|
performAnimation(animations: {
|
||||||
blurEffectView.effect = UIBlurEffect(style: .systemMaterial)
|
blurEffectView.effect = UIBlurEffect(style: .systemMaterial)
|
||||||
fromViewSnapshot.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
fromViewSnapshot.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||||
toView.alpha = 1
|
toView.alpha = 1
|
||||||
toView.transform = .identity
|
toView.transform = .identity
|
||||||
|
fromView.layoutIfNeeded()
|
||||||
|
toView.layoutIfNeeded()
|
||||||
}) { _ in
|
}) { _ in
|
||||||
let success = !transitionContext.transitionWasCancelled
|
let success = !transitionContext.transitionWasCancelled
|
||||||
if !success {
|
if !success {
|
||||||
@@ -157,6 +161,8 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
|||||||
toView.isHidden = false
|
toView.isHidden = false
|
||||||
fromViewSnapshot.removeFromSuperview()
|
fromViewSnapshot.removeFromSuperview()
|
||||||
blurEffectView.removeFromSuperview()
|
blurEffectView.removeFromSuperview()
|
||||||
|
fromView.layoutIfNeeded()
|
||||||
|
toView.layoutIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
fromView.transform = .identity
|
fromView.transform = .identity
|
||||||
|
|||||||
+25
-21
@@ -23,7 +23,9 @@ class MainViewController: UIViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
view.layoutIfNeeded()
|
||||||
setupUI()
|
setupUI()
|
||||||
|
view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
@@ -31,6 +33,11 @@ class MainViewController: UIViewController {
|
|||||||
navigationController!.setNavigationBarHidden(true, animated: animated)
|
navigationController!.setNavigationBarHidden(true, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
inputBox.textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
navigationController!.setNavigationBarHidden(false, animated: animated)
|
navigationController!.setNavigationBarHidden(false, animated: animated)
|
||||||
@@ -41,6 +48,19 @@ class MainViewController: UIViewController {
|
|||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
// 计算 InputBox 的初始 frame 以避免布局动画
|
||||||
|
let inputBoxFrame = CGRect(
|
||||||
|
x: 0,
|
||||||
|
y: view.bounds.height - 150, // 预估高度
|
||||||
|
width: view.bounds.width,
|
||||||
|
height: 150
|
||||||
|
)
|
||||||
|
let inputBox = InputBox(frame: inputBoxFrame).then {
|
||||||
|
$0.delegate = self
|
||||||
|
}
|
||||||
|
self.inputBox = inputBox
|
||||||
|
self.inputBox.layoutIfNeeded()
|
||||||
|
|
||||||
view.addSubview(headerView)
|
view.addSubview(headerView)
|
||||||
view.addSubview(inputBox)
|
view.addSubview(inputBox)
|
||||||
|
|
||||||
@@ -77,31 +97,15 @@ extension MainViewController: MainHeaderViewDelegate {
|
|||||||
// MARK: - InputBoxDelegate
|
// MARK: - InputBoxDelegate
|
||||||
|
|
||||||
extension MainViewController: InputBoxDelegate {
|
extension MainViewController: InputBoxDelegate {
|
||||||
func inputBoxDidTapAddAttachment() {
|
func inputBoxDidSelectAttachment(_ inputBox: InputBox) {
|
||||||
// TODO: 实现添加附件功能
|
print(#function, inputBox)
|
||||||
print("Add attachment tapped")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputBoxDidTapTool() {
|
func inputBoxDidSend(_ inputBox: InputBox) {
|
||||||
print("Tool toggled: \(inputBox.viewModel.isToolEnabled)")
|
print(#function, inputBox, inputBox.viewModel)
|
||||||
}
|
|
||||||
|
|
||||||
func inputBoxDidTapNetwork() {
|
|
||||||
print("Network toggled: \(inputBox.viewModel.isNetworkEnabled)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func inputBoxDidTapDeepThinking() {
|
|
||||||
print("Deep thinking toggled: \(inputBox.viewModel.isDeepThinkingEnabled)")
|
|
||||||
}
|
|
||||||
|
|
||||||
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(#function, text)
|
||||||
print("Text changed: \(text)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-145
@@ -3,24 +3,13 @@ import SnapKit
|
|||||||
import Then
|
import Then
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol InputBoxDelegate: AnyObject {
|
|
||||||
func inputBoxDidTapAddAttachment()
|
|
||||||
func inputBoxDidTapTool()
|
|
||||||
func inputBoxDidTapNetwork()
|
|
||||||
func inputBoxDidTapDeepThinking()
|
|
||||||
func inputBoxDidTapSend(data: InputBoxData)
|
|
||||||
func inputBoxTextDidChange(_ text: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
class InputBox: UIView {
|
class InputBox: UIView {
|
||||||
weak var delegate: InputBoxDelegate?
|
weak var delegate: InputBoxDelegate?
|
||||||
|
|
||||||
// MARK: - ViewModel
|
|
||||||
|
|
||||||
public let viewModel = InputBoxViewModel()
|
public let viewModel = InputBoxViewModel()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private lazy var containerView = UIView().then {
|
lazy var containerView = UIView().then {
|
||||||
$0.backgroundColor = .systemBackground
|
$0.backgroundColor = .systemBackground
|
||||||
$0.layer.cornerRadius = 12
|
$0.layer.cornerRadius = 12
|
||||||
$0.layer.borderWidth = 0.5
|
$0.layer.borderWidth = 0.5
|
||||||
@@ -32,7 +21,7 @@ class InputBox: UIView {
|
|||||||
$0.clipsToBounds = false
|
$0.clipsToBounds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var textView = UITextView().then {
|
lazy var textView = UITextView().then {
|
||||||
$0.backgroundColor = .clear
|
$0.backgroundColor = .clear
|
||||||
$0.font = .systemFont(ofSize: 16)
|
$0.font = .systemFont(ofSize: 16)
|
||||||
$0.textColor = .label
|
$0.textColor = .label
|
||||||
@@ -43,91 +32,30 @@ class InputBox: UIView {
|
|||||||
$0.text = "This is AFFiNE AI"
|
$0.text = "This is AFFiNE AI"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var placeholderLabel = UILabel().then {
|
lazy var placeholderLabel = UILabel().then {
|
||||||
$0.text = "Write your message..."
|
$0.text = "Write your message..."
|
||||||
$0.font = .systemFont(ofSize: 16)
|
$0.font = .systemFont(ofSize: 16)
|
||||||
$0.textColor = .systemGray3
|
$0.textColor = .systemGray3
|
||||||
$0.isHidden = true
|
$0.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var addButton = UIButton(type: .system).then {
|
lazy var functionBar = InputBoxFunctionBar().then {
|
||||||
$0.backgroundColor = .systemBackground
|
$0.delegate = self
|
||||||
$0.layer.cornerRadius = 6
|
|
||||||
$0.layer.borderWidth = 0.5
|
|
||||||
$0.layer.borderColor = UIColor.systemGray4.cgColor
|
|
||||||
$0.setImage(UIImage(named: "inputbox.add.attachment", in: .module, with: nil), for: .normal)
|
|
||||||
$0.tintColor = .secondaryLabel
|
|
||||||
$0.imageView?.contentMode = .scaleAspectFit
|
|
||||||
$0.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var toolButton = UIButton(type: .system).then {
|
lazy var imageBar = InputBoxImageBar(frame: bounds)
|
||||||
$0.setImage(UIImage(named: "inputbox.tool", in: .module, with: nil), for: .normal)
|
|
||||||
$0.tintColor = .secondaryLabel
|
|
||||||
$0.imageView?.contentMode = .scaleAspectFit
|
|
||||||
$0.addTarget(self, action: #selector(toolButtonTapped), for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var webButton = UIButton(type: .system).then {
|
lazy var mainStackView = UIStackView().then {
|
||||||
$0.setImage(UIImage(named: "inputbox.network", in: .module, with: nil), for: .normal)
|
|
||||||
$0.tintColor = .secondaryLabel
|
|
||||||
$0.imageView?.contentMode = .scaleAspectFit
|
|
||||||
$0.addTarget(self, action: #selector(webButtonTapped), for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var reactButton = UIButton(type: .system).then {
|
|
||||||
$0.setImage(UIImage(named: "inputbox.deep.thinking", in: .module, with: nil), for: .normal)
|
|
||||||
$0.tintColor = .secondaryLabel
|
|
||||||
$0.imageView?.contentMode = .scaleAspectFit
|
|
||||||
$0.addTarget(self, action: #selector(reactButtonTapped), for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var sendButton = UIButton(type: .system).then {
|
|
||||||
$0.backgroundColor = UIColor.systemBlue
|
|
||||||
$0.layer.cornerRadius = 19
|
|
||||||
$0.setImage(UIImage(named: "inputbox.send", in: .module, with: nil), for: .normal)
|
|
||||||
$0.tintColor = .white
|
|
||||||
$0.imageView?.contentMode = .scaleAspectFit
|
|
||||||
$0.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var leftButtonsStackView = UIStackView().then {
|
|
||||||
$0.axis = .horizontal
|
|
||||||
$0.spacing = 16
|
|
||||||
$0.alignment = .center
|
|
||||||
$0.addArrangedSubview(addButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var rightButtonsStackView = UIStackView().then {
|
|
||||||
$0.axis = .horizontal
|
|
||||||
$0.spacing = 16
|
|
||||||
$0.alignment = .center
|
|
||||||
$0.addArrangedSubview(toolButton)
|
|
||||||
$0.addArrangedSubview(webButton)
|
|
||||||
$0.addArrangedSubview(reactButton)
|
|
||||||
$0.addArrangedSubview(sendButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var functionsStackView = UIStackView().then {
|
|
||||||
$0.axis = .horizontal
|
|
||||||
$0.spacing = 12
|
|
||||||
$0.alignment = .center
|
|
||||||
$0.addArrangedSubview(leftButtonsStackView)
|
|
||||||
$0.addArrangedSubview(UIView()) // spacer
|
|
||||||
$0.addArrangedSubview(rightButtonsStackView)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var mainStackView = UIStackView().then {
|
|
||||||
$0.axis = .vertical
|
$0.axis = .vertical
|
||||||
$0.spacing = 16
|
$0.spacing = 16
|
||||||
$0.alignment = .fill
|
$0.alignment = .fill
|
||||||
$0.addArrangedSubview(textView)
|
$0.addArrangedSubview(textView)
|
||||||
$0.addArrangedSubview(functionsStackView)
|
$0.addArrangedSubview(functionBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var textViewHeightConstraint: Constraint?
|
var textViewHeightConstraint: Constraint?
|
||||||
private let minTextViewHeight: CGFloat = 22
|
let minTextViewHeight: CGFloat = 22
|
||||||
private let maxTextViewHeight: CGFloat = 100
|
let maxTextViewHeight: CGFloat = 100
|
||||||
|
|
||||||
var text: String {
|
var text: String {
|
||||||
get { textView.text ?? "" }
|
get { textView.text ?? "" }
|
||||||
@@ -137,9 +65,9 @@ class InputBox: UIView {
|
|||||||
updateTextViewHeight()
|
updateTextViewHeight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
override init(frame: CGRect = .zero) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: frame)
|
||||||
setupViews()
|
setupViews()
|
||||||
setupConstraints()
|
setupConstraints()
|
||||||
setupBindings()
|
setupBindings()
|
||||||
@@ -151,14 +79,16 @@ class InputBox: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupViews() {
|
func setupViews() {
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
addSubview(containerView)
|
addSubview(containerView)
|
||||||
containerView.addSubview(mainStackView)
|
containerView.addSubview(mainStackView)
|
||||||
containerView.addSubview(placeholderLabel)
|
containerView.addSubview(placeholderLabel)
|
||||||
|
|
||||||
|
imageBar.imageBarDelegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupBindings() {
|
func setupBindings() {
|
||||||
// 绑定 ViewModel 到 UI
|
// 绑定 ViewModel 到 UI
|
||||||
viewModel.$inputText
|
viewModel.$inputText
|
||||||
.sink { [weak self] text in
|
.sink { [weak self] text in
|
||||||
@@ -172,43 +102,42 @@ class InputBox: UIView {
|
|||||||
|
|
||||||
viewModel.$isToolEnabled
|
viewModel.$isToolEnabled
|
||||||
.sink { [weak self] enabled in
|
.sink { [weak self] enabled in
|
||||||
self?.toolButton.isSelected = enabled
|
self?.functionBar.updateToolState(isEnabled: enabled)
|
||||||
self?.toolButton.tintColor = enabled ? .systemBlue : .secondaryLabel
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$isNetworkEnabled
|
viewModel.$isNetworkEnabled
|
||||||
.sink { [weak self] enabled in
|
.sink { [weak self] enabled in
|
||||||
self?.webButton.isSelected = enabled
|
self?.functionBar.updateNetworkState(isEnabled: enabled)
|
||||||
self?.webButton.tintColor = enabled ? .systemBlue : .secondaryLabel
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$isDeepThinkingEnabled
|
viewModel.$isDeepThinkingEnabled
|
||||||
.sink { [weak self] enabled in
|
.sink { [weak self] enabled in
|
||||||
self?.reactButton.isSelected = enabled
|
self?.functionBar.updateDeepThinkingState(isEnabled: enabled)
|
||||||
self?.reactButton.tintColor = enabled ? .systemBlue : .secondaryLabel
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$canSend
|
viewModel.$canSend
|
||||||
.sink { [weak self] canSend in
|
.sink { [weak self] canSend in
|
||||||
self?.sendButton.isEnabled = canSend
|
self?.functionBar.updateSendState(canSend: canSend)
|
||||||
self?.sendButton.alpha = canSend ? 1.0 : 0.5
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$isSending
|
viewModel.$hasAttachments
|
||||||
.sink { [weak self] isSending in
|
.sink { [weak self] hasAttachments in
|
||||||
self?.sendButton.isEnabled = !isSending
|
self?.updateImageBarVisibility(hasAttachments)
|
||||||
if isSending {
|
}
|
||||||
// TODO: 添加加载动画
|
.store(in: &cancellables)
|
||||||
}
|
|
||||||
|
viewModel.$attachments
|
||||||
|
.sink { [weak self] attachments in
|
||||||
|
self?.updateImageBarContent(attachments)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupConstraints() {
|
func setupConstraints() {
|
||||||
containerView.snp.makeConstraints { make in
|
containerView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(16)
|
make.edges.equalToSuperview().inset(16)
|
||||||
}
|
}
|
||||||
@@ -217,20 +146,6 @@ class InputBox: UIView {
|
|||||||
make.edges.equalToSuperview().inset(16)
|
make.edges.equalToSuperview().inset(16)
|
||||||
}
|
}
|
||||||
|
|
||||||
addButton.snp.makeConstraints { make in
|
|
||||||
make.size.equalTo(38)
|
|
||||||
}
|
|
||||||
|
|
||||||
for button in [toolButton, webButton, reactButton] {
|
|
||||||
button.snp.makeConstraints { make in
|
|
||||||
make.size.equalTo(24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendButton.snp.makeConstraints { make in
|
|
||||||
make.size.equalTo(38)
|
|
||||||
}
|
|
||||||
|
|
||||||
textView.snp.makeConstraints { make in
|
textView.snp.makeConstraints { make in
|
||||||
textViewHeightConstraint = make.height.equalTo(minTextViewHeight).constraint
|
textViewHeightConstraint = make.height.equalTo(minTextViewHeight).constraint
|
||||||
}
|
}
|
||||||
@@ -241,61 +156,93 @@ class InputBox: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTextViewHeight() {
|
func updateTextViewHeight() {
|
||||||
let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude))
|
let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude))
|
||||||
let newHeight = max(minTextViewHeight, min(maxTextViewHeight, size.height))
|
let newHeight = max(minTextViewHeight, min(maxTextViewHeight, size.height))
|
||||||
|
|
||||||
|
let height = textView.frame.height
|
||||||
|
guard height != newHeight else { return }
|
||||||
|
|
||||||
textViewHeightConstraint?.update(offset: newHeight)
|
textViewHeightConstraint?.update(offset: newHeight)
|
||||||
textView.isScrollEnabled = size.height > maxTextViewHeight
|
textView.isScrollEnabled = size.height > maxTextViewHeight
|
||||||
|
|
||||||
|
if height == 0 || superview == nil || window == nil || isHidden { return }
|
||||||
|
|
||||||
UIView.animate(
|
UIView.animate(
|
||||||
withDuration: 0.5,
|
withDuration: 0.5,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
usingSpringWithDamping: 0.8,
|
usingSpringWithDamping: 0.8,
|
||||||
initialSpringVelocity: 1.0,
|
initialSpringVelocity: 1.0,
|
||||||
options: [.curveEaseInOut]
|
options: .curveEaseInOut
|
||||||
) {
|
) {
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
self.superview?.layoutIfNeeded()
|
self.superview?.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePlaceholderVisibility() {
|
func updatePlaceholderVisibility() {
|
||||||
placeholderLabel.isHidden = !textView.text.isEmpty
|
placeholderLabel.isHidden = !textView.text.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func addButtonTapped() {
|
func updateImageBarVisibility(_ hasAttachments: Bool) {
|
||||||
delegate?.inputBoxDidTapAddAttachment()
|
if hasAttachments, !mainStackView.arrangedSubviews.contains(imageBar) {
|
||||||
|
mainStackView.insertArrangedSubview(imageBar, at: 1)
|
||||||
|
} else if !hasAttachments, mainStackView.arrangedSubviews.contains(imageBar) {
|
||||||
|
mainStackView.removeArrangedSubview(imageBar)
|
||||||
|
imageBar.removeFromSuperview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func toolButtonTapped() {
|
func updateImageBarContent(_ attachments: [InputAttachment]) {
|
||||||
|
imageBar.clear()
|
||||||
|
|
||||||
|
for attachment in attachments {
|
||||||
|
if attachment.type == .image, let data = attachment.data, let image = UIImage(data: data) {
|
||||||
|
imageBar.addImage(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
public func addImageAttachment(_ image: UIImage) {
|
||||||
|
guard let imageData = image.jpegData(compressionQuality: 0.8) else { return }
|
||||||
|
|
||||||
|
let attachment = InputAttachment(
|
||||||
|
type: .image,
|
||||||
|
data: imageData,
|
||||||
|
name: "image.jpg",
|
||||||
|
size: Int64(imageData.count)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.addAttachment(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var inputBoxData: InputBoxData {
|
||||||
|
viewModel.prepareSendData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - InputBoxFunctionBarDelegate
|
||||||
|
|
||||||
|
extension InputBox: InputBoxFunctionBarDelegate {
|
||||||
|
func functionBarDidTapAttachment(_: InputBoxFunctionBar) {
|
||||||
|
delegate?.inputBoxDidSelectAttachment(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func functionBarDidTapTool(_: InputBoxFunctionBar) {
|
||||||
viewModel.toggleTool()
|
viewModel.toggleTool()
|
||||||
delegate?.inputBoxDidTapTool()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func webButtonTapped() {
|
func functionBarDidTapNetwork(_: InputBoxFunctionBar) {
|
||||||
viewModel.toggleNetwork()
|
viewModel.toggleNetwork()
|
||||||
delegate?.inputBoxDidTapNetwork()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func reactButtonTapped() {
|
func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) {
|
||||||
viewModel.toggleDeepThinking()
|
viewModel.toggleDeepThinking()
|
||||||
delegate?.inputBoxDidTapDeepThinking()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sendButtonTapped() {
|
func functionBarDidTapSend(_: InputBoxFunctionBar) {
|
||||||
let data = viewModel.prepareSendData()
|
delegate?.inputBoxDidSend(self)
|
||||||
delegate?.inputBoxDidTapSend(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITextViewDelegate
|
|
||||||
|
|
||||||
extension InputBox: UITextViewDelegate {
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
|
||||||
updatePlaceholderVisibility()
|
|
||||||
updateTextViewHeight()
|
|
||||||
viewModel.updateText(textView.text)
|
|
||||||
delegate?.inputBoxTextDidChange(textView.text)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// InputBoxDelegate.swift
|
||||||
|
// Intelligents
|
||||||
|
//
|
||||||
|
// Created by 秋星桥 on 6/18/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol InputBoxDelegate: AnyObject {
|
||||||
|
func inputBoxDidSelectAttachment(_ inputBox: InputBox)
|
||||||
|
func inputBoxDidSend(_ inputBox: InputBox)
|
||||||
|
func inputBoxTextDidChange(_ text: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InputBox: InputBoxImageBarDelegate {
|
||||||
|
func inputBoxImageBar(_: InputBoxImageBar, didRemoveImageAt index: Int) {
|
||||||
|
viewModel.removeAttachment(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InputBox: UITextViewDelegate {
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
viewModel.updateText(textView.text ?? "")
|
||||||
|
delegate?.inputBoxTextDidChange(textView.text ?? "")
|
||||||
|
updatePlaceholderVisibility()
|
||||||
|
updateTextViewHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
import SnapKit
|
||||||
|
import Then
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol InputBoxFunctionBarDelegate: AnyObject {
|
||||||
|
func functionBarDidTapAttachment(_ functionBar: InputBoxFunctionBar)
|
||||||
|
func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar)
|
||||||
|
func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar)
|
||||||
|
func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar)
|
||||||
|
func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
class InputBoxFunctionBar: UIView {
|
||||||
|
weak var delegate: InputBoxFunctionBarDelegate?
|
||||||
|
|
||||||
|
lazy var attachmentButton = UIButton(type: .system).then {
|
||||||
|
$0.backgroundColor = .systemBackground
|
||||||
|
$0.setImage(UIImage(named: "inputbox.add.attachment", in: .module, with: nil), for: .normal)
|
||||||
|
$0.tintColor = .secondaryLabel
|
||||||
|
$0.imageView?.contentMode = .scaleAspectFit
|
||||||
|
$0.addTarget(self, action: #selector(attachmentButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var toolButton = UIButton(type: .system).then {
|
||||||
|
$0.setImage(UIImage(named: "inputbox.tool", in: .module, with: nil), for: .normal)
|
||||||
|
$0.tintColor = .secondaryLabel
|
||||||
|
$0.imageView?.contentMode = .scaleAspectFit
|
||||||
|
$0.addTarget(self, action: #selector(toolButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var networkButton = UIButton(type: .system).then {
|
||||||
|
$0.setImage(UIImage(named: "inputbox.network", in: .module, with: nil), for: .normal)
|
||||||
|
$0.tintColor = .secondaryLabel
|
||||||
|
$0.imageView?.contentMode = .scaleAspectFit
|
||||||
|
$0.addTarget(self, action: #selector(networkButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var deepThinkingButton = UIButton(type: .system).then {
|
||||||
|
$0.setImage(UIImage(named: "inputbox.deep.thinking", in: .module, with: nil), for: .normal)
|
||||||
|
$0.tintColor = .secondaryLabel
|
||||||
|
$0.imageView?.contentMode = .scaleAspectFit
|
||||||
|
$0.addTarget(self, action: #selector(deepThinkingButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var sendButton = UIButton(type: .system).then {
|
||||||
|
$0.backgroundColor = UIColor.systemBlue
|
||||||
|
$0.setImage(UIImage(named: "inputbox.send", in: .module, with: nil), for: .normal)
|
||||||
|
$0.tintColor = .white
|
||||||
|
$0.imageView?.contentMode = .scaleAspectFit
|
||||||
|
$0.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var leftButtonsStackView = UIStackView().then {
|
||||||
|
$0.axis = .horizontal
|
||||||
|
$0.spacing = 16
|
||||||
|
$0.alignment = .center
|
||||||
|
$0.addArrangedSubview(attachmentButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var rightButtonsStackView = UIStackView().then {
|
||||||
|
$0.axis = .horizontal
|
||||||
|
$0.spacing = 16
|
||||||
|
$0.alignment = .center
|
||||||
|
$0.addArrangedSubview(toolButton)
|
||||||
|
$0.addArrangedSubview(networkButton)
|
||||||
|
$0.addArrangedSubview(deepThinkingButton)
|
||||||
|
$0.addArrangedSubview(sendButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var stackView = UIStackView().then {
|
||||||
|
$0.axis = .horizontal
|
||||||
|
$0.spacing = 12
|
||||||
|
$0.alignment = .center
|
||||||
|
$0.addArrangedSubview(leftButtonsStackView)
|
||||||
|
$0.addArrangedSubview(UIView()) // spacer
|
||||||
|
$0.addArrangedSubview(rightButtonsStackView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupViews()
|
||||||
|
setupConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupViews() {
|
||||||
|
addSubview(stackView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
stackView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
for button in [attachmentButton, toolButton, networkButton, deepThinkingButton, sendButton] {
|
||||||
|
button.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
sendButton.layer.cornerRadius = sendButton.bounds.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
func updateToolState(isEnabled: Bool) {
|
||||||
|
toolButton.isSelected = isEnabled
|
||||||
|
toolButton.tintColor = isEnabled ? .systemBlue : .secondaryLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNetworkState(isEnabled: Bool) {
|
||||||
|
networkButton.isSelected = isEnabled
|
||||||
|
networkButton.tintColor = isEnabled ? .systemBlue : .secondaryLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDeepThinkingState(isEnabled: Bool) {
|
||||||
|
deepThinkingButton.isSelected = isEnabled
|
||||||
|
deepThinkingButton.tintColor = isEnabled ? .systemBlue : .secondaryLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSendState(canSend: Bool) {
|
||||||
|
sendButton.isEnabled = canSend
|
||||||
|
sendButton.alpha = canSend ? 1.0 : 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
@objc private func attachmentButtonTapped() {
|
||||||
|
delegate?.functionBarDidTapAttachment(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func toolButtonTapped() {
|
||||||
|
delegate?.functionBarDidTapTool(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func networkButtonTapped() {
|
||||||
|
delegate?.functionBarDidTapNetwork(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func deepThinkingButtonTapped() {
|
||||||
|
delegate?.functionBarDidTapDeepThinking(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func sendButtonTapped() {
|
||||||
|
delegate?.functionBarDidTapSend(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
//
|
||||||
|
// InputBoxImageBar.swift
|
||||||
|
// Intelligents
|
||||||
|
//
|
||||||
|
// Created by 秋星桥 on 6/18/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import Then
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol InputBoxImageBarDelegate: AnyObject {
|
||||||
|
func inputBoxImageBar(_ imageBar: InputBoxImageBar, didRemoveImageAt index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let constantHeight: CGFloat = 108
|
||||||
|
|
||||||
|
class InputBoxImageBar: UIScrollView {
|
||||||
|
weak var imageBarDelegate: InputBoxImageBarDelegate?
|
||||||
|
|
||||||
|
private lazy var stackView = UIStackView().then {
|
||||||
|
$0.axis = .horizontal
|
||||||
|
$0.spacing = 8
|
||||||
|
$0.alignment = .center
|
||||||
|
$0.distribution = .equalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
private var imageCells: [ImageCell] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect = .zero) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupViews()
|
||||||
|
setupConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupViews() {
|
||||||
|
showsHorizontalScrollIndicator = false
|
||||||
|
showsVerticalScrollIndicator = false
|
||||||
|
addSubview(stackView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
stackView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
make.height.equalTo(constantHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(constantHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addImage(_ image: UIImage) {
|
||||||
|
let imageCell = ImageCell(image: image)
|
||||||
|
imageCell.onRemove = { [weak self] cell in
|
||||||
|
self?.removeImageCell(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCells.append(imageCell)
|
||||||
|
stackView.addArrangedSubview(imageCell)
|
||||||
|
updateContentSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeImageCell(_ cell: ImageCell) {
|
||||||
|
if let index = imageCells.firstIndex(of: cell) {
|
||||||
|
imageCells.remove(at: index)
|
||||||
|
stackView.removeArrangedSubview(cell)
|
||||||
|
cell.removeFromSuperview()
|
||||||
|
imageBarDelegate?.inputBoxImageBar(self, didRemoveImageAt: index)
|
||||||
|
updateContentSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
for cell in imageCells {
|
||||||
|
stackView.removeArrangedSubview(cell)
|
||||||
|
cell.removeFromSuperview()
|
||||||
|
}
|
||||||
|
imageCells.removeAll()
|
||||||
|
updateContentSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateContentSize() {
|
||||||
|
layoutIfNeeded()
|
||||||
|
contentSize = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InputBoxImageBar {
|
||||||
|
class ImageCell: UIView {
|
||||||
|
var onRemove: ((ImageCell) -> Void)?
|
||||||
|
|
||||||
|
private lazy var imageView = UIImageView().then {
|
||||||
|
$0.contentMode = .scaleAspectFill
|
||||||
|
$0.clipsToBounds = true
|
||||||
|
$0.layer.cornerRadius = 12
|
||||||
|
$0.backgroundColor = .systemGray6
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var removeButton = UIButton(type: .system).then {
|
||||||
|
$0.backgroundColor = UIColor.black.withAlphaComponent(0.52)
|
||||||
|
$0.layer.cornerRadius = 8.5
|
||||||
|
$0.layer.borderWidth = 1
|
||||||
|
$0.layer.borderColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1).cgColor
|
||||||
|
$0.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||||
|
$0.tintColor = .white
|
||||||
|
$0.addTarget(self, action: #selector(removeButtonTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(image: UIImage) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
setupViews()
|
||||||
|
setupConstraints()
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupViews() {
|
||||||
|
addSubview(imageView)
|
||||||
|
addSubview(removeButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
// 设置固定高度和1:1宽高比
|
||||||
|
snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(constantHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
removeButton.snp.makeConstraints { make in
|
||||||
|
make.top.trailing.equalToSuperview().inset(6.5)
|
||||||
|
make.width.height.equalTo(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func removeButtonTapped() {
|
||||||
|
onRemove?(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+136
-129
@@ -8,123 +8,23 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class InputBoxViewModel: ObservableObject {
|
// MARK: - Data Models
|
||||||
// MARK: - Published Properties
|
|
||||||
|
|
||||||
@Published public var inputText: String = ""
|
public enum AttachmentType: Equatable {
|
||||||
@Published public var isToolEnabled: Bool = false
|
case image
|
||||||
@Published public var isNetworkEnabled: Bool = false
|
case document
|
||||||
@Published public var isDeepThinkingEnabled: Bool = false
|
case video
|
||||||
@Published public var hasAttachments: Bool = false
|
case audio
|
||||||
@Published public var attachments: [InputAttachment] = []
|
case other(String)
|
||||||
@Published public var isSending: Bool = false
|
|
||||||
|
|
||||||
// MARK: - Private Properties
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
case .image: "Image"
|
||||||
|
case .document: "Document"
|
||||||
// MARK: - Initialization
|
case .video: "Video"
|
||||||
|
case .audio: "Audio"
|
||||||
public init() {
|
case let .other(type): type
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,20 +53,127 @@ public struct InputAttachment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AttachmentType {
|
public struct InputBoxData {
|
||||||
case image
|
public let text: String
|
||||||
case document
|
public let attachments: [InputAttachment]
|
||||||
case video
|
public let isToolEnabled: Bool
|
||||||
case audio
|
public let isNetworkEnabled: Bool
|
||||||
case other(String)
|
public let isDeepThinkingEnabled: Bool
|
||||||
|
|
||||||
public var displayName: String {
|
public init(
|
||||||
switch self {
|
text: String,
|
||||||
case .image: "Image"
|
attachments: [InputAttachment],
|
||||||
case .document: "Document"
|
isToolEnabled: Bool,
|
||||||
case .video: "Video"
|
isNetworkEnabled: Bool,
|
||||||
case .audio: "Audio"
|
isDeepThinkingEnabled: Bool
|
||||||
case let .other(type): type
|
) {
|
||||||
}
|
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(at index: Int) {
|
||||||
|
guard index < attachments.count else { return }
|
||||||
|
attachments.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user