diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj
index 44b56f612f..92a24b391b 100644
--- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj
+++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj
@@ -514,14 +514,13 @@
baseConfigurationReference = 3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_IDENTITY = "Apple Distribution: TOEVERYTHING PTE. LTD. (73YMMDVT2M)";
- CODE_SIGN_STYLE = Manual;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
- DEVELOPMENT_TEAM = 73YMMDVT2M;
- "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
+ DEVELOPMENT_TEAM = 964G86XT2P;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -559,7 +558,7 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -572,8 +571,7 @@
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "AppStore app.affine.pro";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "AppStore app.affine.pro";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 6c76aeb265..0973b9f21f 100644
--- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -45,6 +45,15 @@
"version" : "1.0.4"
}
},
+ {
+ "identity" : "swifterswift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SwifterSwift/SwifterSwift.git",
+ "state" : {
+ "revision" : "39fa28c90a3ebe3d53f80289304fd880cf2c42d0",
+ "version" : "6.2.0"
+ }
+ },
{
"identity" : "then",
"kind" : "remoteSourceControl",
diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift
index ddc1208580..22227845ab 100644
--- a/packages/frontend/apps/ios/App/App/AffineViewController.swift
+++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift
@@ -11,7 +11,7 @@ class AFFiNEViewController: CAPBridgeViewController {
edgesForExtendedLayout = []
let intelligentsButton = installIntelligentsButton()
intelligentsButton.delegate = self
- presentIntelligentsButton() // from v2.0 always visible
+ dismissIntelligentsButton()
}
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
@@ -38,7 +38,11 @@ class AFFiNEViewController: CAPBridgeViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ self.presentIntelligentsButton()
+ }
}
}
+
diff --git a/packages/frontend/apps/ios/App/App/Info.plist b/packages/frontend/apps/ios/App/App/Info.plist
index ff1e14a218..7bc4ac16e4 100644
--- a/packages/frontend/apps/ios/App/App/Info.plist
+++ b/packages/frontend/apps/ios/App/App/Info.plist
@@ -42,7 +42,7 @@
NSPhotoLibraryUsageDescription
AFFiNE requires access to select photos from your photo library and insert them into your documents
NSUserTrackingUsageDescription
- Rest assured, enabling this permission won't access your private info on other sites. It's only used to identify your device and improve security and product experience.
+ Rest assured, enabling this permission won't access your private info on other sites. It's only used to identify your device and improve security and product experience.
UILaunchScreen
UIImageName
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift
index 76dd137cf9..e8104d16eb 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift
@@ -7,7 +7,7 @@ let package = Package(
name: "Intelligents",
defaultLocalization: "en",
platforms: [
- .iOS(.v17),
+ .iOS(.v16),
],
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
@@ -18,16 +18,19 @@ let package = Package(
.package(url: "https://github.com/apple/swift-collections", from: "1.2.0"),
.package(url: "https://github.com/devxoul/Then", from: "3.0.0"),
.package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.1"),
+ .package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"),
],
targets: [
.target(name: "Intelligents", dependencies: [
"AffineGraphQL",
"SnapKit",
"Then",
+ "SwifterSwift",
.product(name: "Apollo", package: "apollo-ios"),
.product(name: "OrderedCollections", package: "swift-collections"),
], resources: [
- .process("Interface/View/InputBox/InputBox.xcassets")
+ .process("Resources/main.metal"),
+ .process("Interface/View/InputBox/InputBox.xcassets"),
]),
]
)
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift
index 02587fa28a..f2929ea946 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift
@@ -35,7 +35,6 @@ class BlurTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate
class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let presenting: Bool
- private let duration: TimeInterval = 0.5
private let snapshotViewTag = "snapshotView".hashValue
private let blurViewTag = "blurView".hashValue
@@ -45,23 +44,8 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
super.init()
}
- private func performAnimation(
- animations: @escaping () -> Void,
- completion: @escaping (Bool) -> Void
- ) {
- UIView.animate(
- withDuration: duration,
- delay: 0,
- usingSpringWithDamping: 0.75,
- initialSpringVelocity: 0.75,
- options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut],
- animations: animations,
- completion: completion
- )
- }
-
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
- duration
+ 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
@@ -106,11 +90,15 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
toView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
containerView.addSubview(toView)
- performAnimation(animations: {
+ toView.layoutIfNeeded()
+
+ performWithAnimation(animations: {
blurEffectView.effect = UIBlurEffect(style: .systemMaterial)
fromViewSnapshot.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
toView.alpha = 1
toView.transform = .identity
+ fromView.layoutIfNeeded()
+ toView.layoutIfNeeded()
}) { _ in
let success = !transitionContext.transitionWasCancelled
if !success {
@@ -146,7 +134,7 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
return
}
- performAnimation(animations: {
+ performWithAnimation(animations: {
fromViewSnapshot.transform = .identity
blurEffectView.effect = nil
fromView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
@@ -157,6 +145,8 @@ class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
toView.isHidden = false
fromViewSnapshot.removeFromSuperview()
blurEffectView.removeFromSuperview()
+ fromView.layoutIfNeeded()
+ toView.layoutIfNeeded()
} else {
assertionFailure()
fromView.transform = .identity
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift
new file mode 100644
index 0000000000..36fb59af9f
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift
@@ -0,0 +1,22 @@
+//
+// MainViewController+Header.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/19/25.
+//
+
+import UIKit
+
+extension MainViewController: MainHeaderViewDelegate {
+ func mainHeaderViewDidTapClose() {
+ dismiss(animated: true)
+ }
+
+ func mainHeaderViewDidTapDropdown() {
+ print(#function)
+ }
+
+ func mainHeaderViewDidTapMenu() {
+ print(#function)
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift
new file mode 100644
index 0000000000..c1304562d2
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift
@@ -0,0 +1,123 @@
+//
+// MainViewController+Input.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/19/25.
+//
+
+import PhotosUI
+import UIKit
+import UniformTypeIdentifiers
+
+extension MainViewController: InputBoxDelegate {
+ func inputBoxDidSelectTakePhoto(_: InputBox) {
+ let imagePickerController = UIImagePickerController()
+ imagePickerController.delegate = self
+ imagePickerController.sourceType = .camera
+ imagePickerController.allowsEditing = false
+ present(imagePickerController, animated: true)
+ }
+
+ func inputBoxDidSelectPhotoLibrary(_: InputBox) {
+ var configuration = PHPickerConfiguration()
+ configuration.filter = .images
+ configuration.selectionLimit = 0 // 0 means no limit
+
+ let picker = PHPickerViewController(configuration: configuration)
+ picker.delegate = self
+ present(picker, animated: true)
+ }
+
+ func inputBoxDidSelectAttachFiles(_: InputBox) {
+ let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [
+ .pdf, .plainText, .commaSeparatedText, .data,
+ ])
+ documentPicker.delegate = self
+ documentPicker.allowsMultipleSelection = false
+ present(documentPicker, animated: true)
+ }
+
+ func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox) {
+ print(#function, inputBox)
+ }
+
+ func inputBoxDidSelectAttachment(_ inputBox: InputBox) {
+ print(#function, inputBox)
+ }
+
+ func inputBoxDidSend(_ inputBox: InputBox) {
+ print(#function, inputBox, inputBox.viewModel)
+ }
+
+ func inputBoxTextDidChange(_ text: String) {
+ print(#function, text)
+ }
+}
+
+// MARK: - UIImagePickerControllerDelegate
+
+extension MainViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+ func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
+ defer { picker.dismiss(animated: true) }
+
+ guard let image = info[.originalImage] as? UIImage else { return }
+ inputBox.addImageAttachment(image)
+ }
+
+ func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+ picker.dismiss(animated: true)
+ }
+}
+
+// MARK: - PHPickerViewControllerDelegate
+
+extension MainViewController: PHPickerViewControllerDelegate {
+ func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+ defer { picker.dismiss(animated: true) }
+
+ for result in results {
+ if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
+ result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
+ guard let image = object as? UIImage, error == nil else { return }
+
+ DispatchQueue.main.async {
+ self?.inputBox.addImageAttachment(image)
+ }
+ }
+ }
+ }
+ }
+}
+
+// MARK: - UIDocumentPickerDelegate
+
+extension MainViewController: UIDocumentPickerDelegate {
+ func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
+ for url in urls {
+ // Start accessing security-scoped resource
+ guard url.startAccessingSecurityScopedResource() else { continue }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ // Copy file to temporary directory
+ let context = IntelligentContext.shared
+ context.prepareTemporaryDirectory()
+
+ let tempURL = context.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
+
+ do {
+ // Remove existing file if it exists
+ if FileManager.default.fileExists(atPath: tempURL.path) {
+ try FileManager.default.removeItem(at: tempURL)
+ }
+
+ // Copy file to temporary directory
+ try FileManager.default.copyItem(at: url, to: tempURL)
+
+ // Add file attachment using the temporary URL
+ inputBox.addFileAttachment(tempURL)
+ } catch {
+ print("Failed to copy file: \(error)")
+ }
+ }
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift
index 0698ce2c02..46ea8a009f 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift
@@ -1,3 +1,4 @@
+import Combine
import SnapKit
import Then
import UIKit
@@ -5,35 +6,28 @@ import UIKit
class MainViewController: UIViewController {
// MARK: - UI Components
- private lazy var headerView = MainHeaderView().then {
+ lazy var headerView = MainHeaderView().then {
$0.delegate = self
}
- private lazy var inputBox = InputBox().then {
+ lazy var inputBox = InputBox().then {
$0.delegate = self
}
+ // MARK: - Properties
+
+ private var cancellables = Set()
+ private let intelligentContext = IntelligentContext.shared
+
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
- setupUI()
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- navigationController!.setNavigationBarHidden(true, animated: animated)
- }
-
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- navigationController!.setNavigationBarHidden(false, animated: animated)
- }
-
- // MARK: - Setup
-
- private func setupUI() {
view.backgroundColor = .systemBackground
+ let inputBox = InputBox().then {
+ $0.delegate = self
+ }
+ self.inputBox = inputBox
view.addSubview(headerView)
view.addSubview(inputBox)
@@ -48,51 +42,17 @@ class MainViewController: UIViewController {
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
}
}
-}
-// MARK: - MainHeaderViewDelegate
-
-extension MainViewController: MainHeaderViewDelegate {
- func mainHeaderViewDidTapClose() {
- dismiss(animated: true)
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ navigationController!.setNavigationBarHidden(true, animated: animated)
+ DispatchQueue.main.async {
+ self.inputBox.textView.becomeFirstResponder()
+ }
}
- func mainHeaderViewDidTapDropdown() {
- // TODO: 实现下拉功能
- print("Dropdown tapped")
- }
-
- func mainHeaderViewDidTapMenu() {
- // TODO: 实现菜单功能
- print("Menu tapped")
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ navigationController!.setNavigationBarHidden(false, animated: animated)
}
}
-
-// MARK: - InputBoxDelegate
-
-extension MainViewController: InputBoxDelegate {
- func inputBoxDidTapAddAttachment() {
-
- }
-
- func inputBoxDidTapTool() {
-
- }
-
- func inputBoxDidTapNetwork() {
-
- }
-
- func inputBoxDidTapDeepThinking() {
-
- }
-
- func inputBoxDidTapSend() {
-
- }
-
- func inputBoxTextDidChange(_ text: String) {
-
- }
-
-}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift
new file mode 100644
index 0000000000..51e1e621cf
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift
@@ -0,0 +1,45 @@
+//
+// ParticleView+Removal.swift
+// UIEffectKit
+//
+// Created by 秋星桥 on 6/13/25.
+//
+
+import UIKit
+
+public extension UIView {
+ func removeFromSuperviewWithExplodeEffect() {
+ guard let superview else { return }
+ guard let window else {
+ removeFromSuperview()
+ return
+ }
+ guard MTLCreateSystemDefaultDevice() != nil else {
+ removeFromSuperview()
+ return
+ }
+
+ let image = createViewSnapshot()
+ guard let cgImage = image.cgImage else {
+ removeFromSuperview()
+ return
+ }
+
+ let frameInWindow = superview.convert(frame, to: window)
+ let particleView = ParticleView(frame: frameInWindow)
+
+ window.addSubview(particleView)
+ particleView.layer.zPosition = 1000
+ particleView.frame = frameInWindow
+ particleView.setNeedsLayout()
+ particleView.layoutIfNeeded()
+
+ particleView.beginWith(cgImage, targetFrame: frameInWindow, onComplete: {
+ particleView.removeFromSuperview()
+ }, onFirstFrameRendered: { [weak self] in
+ DispatchQueue.main.async {
+ self?.removeFromSuperview()
+ }
+ })
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift
new file mode 100644
index 0000000000..4e7e1bf676
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift
@@ -0,0 +1,331 @@
+//
+// ParticleView+Renderer.swift
+// UIEffectKit
+//
+// Created by 秋星桥 on 6/13/25.
+//
+
+import MetalKit
+
+extension ParticleView {
+ class Renderer: NSObject, MTKViewDelegate {
+ private struct Particle {
+ var position: simd_float2
+ var velocity: simd_float2
+ var life: simd_float1
+ var duration: simd_float1
+ }
+
+ private struct Vertex {
+ var position: simd_float4
+ var uv: simd_float2
+ var opacity: simd_float1
+ }
+
+ private var isPrepared = false
+ private var renderPipeline: MTLRenderPipelineState!
+ private var computePipeline: MTLComputePipelineState!
+ private var vertexBuffer: MTLBuffer!
+ private var particleBuffer: MTLBuffer!
+ private var particleCount: Int = 0
+ private var texture: MTLTexture!
+ private var targetFrameSize: simd_float2 = .zero
+ private var stepSize: Float = 0
+ private var commandQueue: MTLCommandQueue!
+ private var maxLife: Float = 0
+ private var onComplete: (() -> Void)?
+ private var onFirstFrameRendered: (() -> Void)?
+ private var hasRenderedFirstFrame = false
+ private var device: MTLDevice!
+
+ func prepareResources(
+ with device: MTLDevice,
+ image: CGImage,
+ targetFrame: CGRect,
+ onComplete: @escaping () -> Void,
+ onFirstFrameRendered: @escaping () -> Void
+ ) {
+ guard !isPrepared else { return }
+
+ self.device = device
+ self.onComplete = onComplete
+ self.onFirstFrameRendered = onFirstFrameRendered
+ let integralTargetFrame = targetFrame.integral
+
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ guard let self else { return }
+
+ setupPipelineStates(with: device)
+ setupVertexBuffer(with: device)
+ setupParticleSystem(targetFrame: integralTargetFrame, device: device)
+ setupTexture(from: image, device: device)
+ finalizeSetup(targetFrame: integralTargetFrame, device: device)
+
+ DispatchQueue.main.async { self.isPrepared = true }
+ }
+ }
+
+ private func setupPipelineStates(with device: MTLDevice) {
+ let library = try! device.makeDefaultLibrary(bundle: .module)
+
+ let particleVertexFunction = library.makeFunction(name: "PTS_ParticleVertex")!
+ let particleFragmentFunction = library.makeFunction(name: "PTS_ParticleFragment")!
+ let updateParticlesFunction = library.makeFunction(name: "PTS_UpdateParticles")!
+
+ let renderPipelineDescriptor = createRenderPipelineDescriptor(
+ vertexFunction: particleVertexFunction,
+ fragmentFunction: particleFragmentFunction
+ )
+
+ do {
+ renderPipeline = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
+ computePipeline = try device.makeComputePipelineState(function: updateParticlesFunction)
+ } catch {
+ fatalError("failed to create pipeline states: \(error)")
+ }
+ }
+
+ private func createRenderPipelineDescriptor(
+ vertexFunction: MTLFunction,
+ fragmentFunction: MTLFunction
+ ) -> MTLRenderPipelineDescriptor {
+ let descriptor = MTLRenderPipelineDescriptor()
+ descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+ descriptor.colorAttachments[0].isBlendingEnabled = true
+ descriptor.colorAttachments[0].rgbBlendOperation = .add
+ descriptor.colorAttachments[0].alphaBlendOperation = .add
+ descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
+ descriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
+ descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
+ descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
+ descriptor.vertexFunction = vertexFunction
+ descriptor.fragmentFunction = fragmentFunction
+ return descriptor
+ }
+
+ func mtkView(_: MTKView, drawableSizeWillChange _: CGSize) {
+ // No-op since view is not subject to resize
+ }
+
+ func draw(in view: MTKView) {
+ guard isPrepared else { return }
+
+ updateParticles()
+
+ if checkAllParticlesDead() {
+ DispatchQueue.main.async { [weak self] in
+ self?.onComplete?()
+ }
+ return
+ }
+
+ renderParticles(in: view)
+
+ if !hasRenderedFirstFrame {
+ hasRenderedFirstFrame = true
+ DispatchQueue.main.async { [weak self] in
+ self?.onFirstFrameRendered?()
+ }
+ }
+ }
+
+ private func updateParticles() {
+ let maxThreadsPerThreadgroup = computePipeline.maxTotalThreadsPerThreadgroup
+ let threadgroupSize = min(maxThreadsPerThreadgroup, 2048)
+ let threadgroupCount = (particleCount + threadgroupSize - 1) / threadgroupSize
+
+ let computeCommandBuffer = commandQueue.makeCommandBuffer()!
+
+ let computeCommandEncoder = computeCommandBuffer.makeComputeCommandEncoder()!
+ computeCommandEncoder.setComputePipelineState(computePipeline)
+ computeCommandEncoder.setBuffer(particleBuffer, offset: 0, index: 0)
+ computeCommandEncoder.dispatchThreadgroups(
+ .init(width: threadgroupCount, height: 1, depth: 1),
+ threadsPerThreadgroup: .init(width: threadgroupSize, height: 1, depth: 1)
+ )
+ computeCommandEncoder.endEncoding()
+
+ computeCommandBuffer.commit()
+ }
+
+ private func checkAllParticlesDead() -> Bool {
+ let particleData = particleBuffer
+ .contents()
+ .bindMemory(to: Particle.self, capacity: particleCount)
+
+ for i in 0 ..< particleCount {
+ if particleData[i].life >= 0 {
+ return false
+ }
+ }
+ return true
+ }
+
+ private func renderParticles(in view: MTKView) {
+ let viewCGSize = view.bounds.size
+ var viewSize = simd_float2(Float(viewCGSize.width), Float(viewCGSize.height))
+
+ let renderCommandBuffer = commandQueue.makeCommandBuffer()!
+
+ guard let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
+ renderPassDescriptor.colorAttachments[0].loadAction = .clear
+ renderPassDescriptor.colorAttachments[0].clearColor = .init(red: 0, green: 0, blue: 0, alpha: 0)
+
+ let renderCommandEncoder = renderCommandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
+ renderCommandEncoder.setRenderPipelineState(renderPipeline)
+ renderCommandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
+
+ withUnsafeBytes(of: &viewSize) { pointer in
+ renderCommandEncoder.setVertexBytes(
+ pointer.baseAddress!,
+ length: MemoryLayout.size,
+ index: 1
+ )
+ }
+ renderCommandEncoder.setVertexBuffer(particleBuffer, offset: 0, index: 2)
+ withUnsafeBytes(of: &targetFrameSize) { pointer in
+ renderCommandEncoder.setVertexBytes(
+ pointer.baseAddress!,
+ length: MemoryLayout.size,
+ index: 3
+ )
+ }
+ withUnsafeBytes(of: &stepSize) { pointer in
+ renderCommandEncoder.setVertexBytes(
+ pointer.baseAddress!,
+ length: MemoryLayout.size,
+ index: 4
+ )
+ }
+ renderCommandEncoder.setFragmentTexture(texture, index: 0)
+
+ setupSampler(renderCommandEncoder: renderCommandEncoder)
+
+ renderCommandEncoder.drawPrimitives(
+ type: .triangleStrip,
+ vertexStart: 0,
+ vertexCount: 4,
+ instanceCount: particleCount
+ )
+ renderCommandEncoder.endEncoding()
+
+ renderCommandBuffer.present(view.currentDrawable!)
+ renderCommandBuffer.commit()
+ }
+
+ private func setupSampler(renderCommandEncoder: MTLRenderCommandEncoder) {
+ let samplerDescriptor = MTLSamplerDescriptor()
+ samplerDescriptor.minFilter = .linear
+ samplerDescriptor.magFilter = .linear
+ samplerDescriptor.mipFilter = .notMipmapped
+ samplerDescriptor.sAddressMode = .clampToEdge
+ samplerDescriptor.tAddressMode = .clampToEdge
+ let samplerState = device.makeSamplerState(descriptor: samplerDescriptor)
+ renderCommandEncoder.setFragmentSamplerState(samplerState, index: 0)
+ }
+ }
+}
+
+extension ParticleView.Renderer {
+ private func setupVertexBuffer(with device: MTLDevice) {
+ let vertices: [Vertex] = [
+ .init(position: .init(0, 0, 0, 1), uv: .init(0, 0), opacity: .zero),
+ .init(position: .init(1, 0, 0, 1), uv: .init(1, 0), opacity: .zero),
+ .init(position: .init(0, 1, 0, 1), uv: .init(0, 1), opacity: .zero),
+ .init(position: .init(1, 1, 0, 1), uv: .init(1, 1), opacity: .zero),
+ ]
+ let vertexBuffer = vertices.withUnsafeBytes { pointer in
+ device.makeBuffer(
+ bytes: pointer.baseAddress!,
+ length: MemoryLayout.stride * vertices.count,
+ options: .storageModeShared
+ )
+ }
+ self.vertexBuffer = vertexBuffer!
+ }
+
+ private func setupParticleSystem(targetFrame: CGRect, device: MTLDevice) {
+ var particles = [Particle]()
+ let targetFrameHeight = Float(targetFrame.height)
+ let targetFrameWidth = Float(targetFrame.width)
+ let particleStep = 1
+
+ let estimatedParticleCount = 1
+ * Int(targetFrameWidth / Float(particleStep))
+ * Int(targetFrameHeight / Float(particleStep))
+ let pixelMultiplier = 1
+ particles.reserveCapacity(estimatedParticleCount * pixelMultiplier)
+
+ for y in stride(from: 0, to: Int(targetFrameHeight), by: particleStep) {
+ for x in stride(from: 0, to: Int(targetFrameWidth), by: particleStep) {
+ let particle = createParticle(x: x, y: y, step: particleStep)
+ for _ in 0 ..< pixelMultiplier {
+ particles.append(particle)
+ }
+ }
+ }
+
+ particleCount = particles.count
+ let particleBuffer = particles.withUnsafeBytes { pointer in
+ device.makeBuffer(
+ bytes: pointer.baseAddress!,
+ length: MemoryLayout.stride * particles.count,
+ options: .storageModeShared
+ )
+ }
+ self.particleBuffer = particleBuffer!
+ stepSize = Float(particleStep)
+ }
+
+ private func createParticle(x: Int, y: Int, step: Int) -> Particle {
+ let particleDuration: Float = .random(in: 20 ... 60)
+ let initialX = Float(x) + Float(step) / 2.0
+ let initialY = Float(y) + Float(step) / 2.0
+ return .init(
+ position: .init(initialX, initialY),
+ velocity: .init(
+ cos(Float.random(in: 0 ... (2 * Float.pi))) * Float.random(in: 1 ... 4),
+ sin(Float.random(in: 0 ... (2 * Float.pi))) * Float.random(in: 1 ... 4) - 2.5
+ ),
+ life: simd_float1(particleDuration),
+ duration: simd_float1(particleDuration)
+ )
+ }
+
+ private func setupTexture(from image: CGImage, device: MTLDevice) {
+ let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
+ let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
+
+ guard let context = CGContext(
+ data: nil,
+ width: image.width,
+ height: image.height,
+ bitsPerComponent: 8,
+ bytesPerRow: 0,
+ space: colorSpace,
+ bitmapInfo: bitmapInfo.rawValue
+ ) else { return }
+ context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
+
+ guard let convertedImage = context.makeImage() else { return }
+
+ let textureLoader = MTKTextureLoader(device: device)
+ let textureLoaderOptions: [MTKTextureLoader.Option: Any] = [
+ .textureStorageMode: MTLStorageMode.private.rawValue,
+ .SRGB: false,
+ ]
+ guard let texture = try? textureLoader.newTexture(
+ cgImage: convertedImage,
+ options: textureLoaderOptions
+ ) else { return }
+
+ self.texture = texture
+ }
+
+ private func finalizeSetup(targetFrame: CGRect, device: MTLDevice) {
+ let targetFrameWidth = Float(targetFrame.width)
+ let targetFrameHeight = Float(targetFrame.height)
+ targetFrameSize = .init(targetFrameWidth, targetFrameHeight)
+ commandQueue = device.makeCommandQueue()!
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift
new file mode 100644
index 0000000000..f2c8b666ed
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift
@@ -0,0 +1,79 @@
+//
+// ParticleView.swift
+// TrollNFC
+//
+// Created by 砍砍 on 6/8/25.
+//
+
+import MetalKit
+import simd
+
+import UIKit
+
+class ParticleView: UIView {
+ private var device: MTLDevice!
+ private var metalView: MTKView!
+ private var renderer = Renderer()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupMetalDevice()
+ setupMetalView()
+ setupViewProperties()
+ }
+
+ private func setupMetalDevice() {
+ guard let device = Self.createSystemDefaultDevice() else {
+ fatalError("failed to create Metal device")
+ }
+ self.device = device
+ }
+
+ private func setupMetalView() {
+ metalView = MTKView(frame: .zero, device: device)
+ configureMetalView()
+ addSubview(metalView)
+ }
+
+ private func configureMetalView() {
+ metalView.layer.isOpaque = false
+ metalView.backgroundColor = UIColor.clear
+ metalView.delegate = renderer
+ }
+
+ private func setupViewProperties() {
+ clipsToBounds = false
+ metalView.clipsToBounds = false
+ }
+
+ private static func createSystemDefaultDevice() -> MTLDevice? {
+ MTLCreateSystemDefaultDevice()
+ }
+
+ @available(*, unavailable)
+ required init?(coder _: NSCoder) {
+ fatalError()
+ }
+
+ func beginWith(
+ _ image: CGImage,
+ targetFrame: CGRect,
+ onComplete: @escaping () -> Void,
+ onFirstFrameRendered: @escaping () -> Void
+ ) {
+ renderer.prepareResources(
+ with: device,
+ image: image,
+ targetFrame: targetFrame,
+ onComplete: onComplete,
+ onFirstFrameRendered: onFirstFrameRendered
+ )
+ metalView.draw()
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ let expandedBounds = bounds.insetBy(dx: -bounds.width, dy: -bounds.height)
+ metalView.frame = expandedBounds
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift
new file mode 100644
index 0000000000..a366391784
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift
@@ -0,0 +1,22 @@
+//
+// UIView+createViewSnapshot.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/19/25.
+//
+
+import UIKit
+
+public extension UIView {
+ func createViewSnapshot() -> UIImage {
+ let renderer = UIGraphicsImageRenderer(bounds: bounds)
+ return renderer.image { context in
+ // clear the background
+ context.cgContext.setFillColor(UIColor.clear.cgColor)
+ context.cgContext.fill(bounds)
+
+ // MUST USE DRAW HIERARCHY TO RENDER VISUAL EFFECT VIEW
+ self.drawHierarchy(in: bounds, afterScreenUpdates: false)
+ }
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/AccentColor.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/AccentColor.swift
new file mode 100644
index 0000000000..eec75f6f0a
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/AccentColor.swift
@@ -0,0 +1,12 @@
+//
+// AccentColor.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/19/25.
+//
+
+import UIKit
+
+extension UIColor {
+ static let accent: UIColor = .init(red: 30 / 255, green: 150 / 255, blue: 235 / 255, alpha: 1.0)
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift
new file mode 100644
index 0000000000..bac7c40b81
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift
@@ -0,0 +1,23 @@
+//
+// Animation.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/19/25.
+//
+
+import UIKit
+
+func performWithAnimation(
+ animations: @escaping () -> Void,
+ completion: @escaping (Bool) -> Void = { _ in }
+) {
+ UIView.animate(
+ withDuration: 0.5,
+ delay: 0,
+ usingSpringWithDamping: 0.8,
+ initialSpringVelocity: 0.8,
+ options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut],
+ animations: animations,
+ completion: completion
+ )
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift
index 745188cbec..86d6c65246 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift
@@ -1,32 +1,26 @@
+import Combine
import SnapKit
import Then
import UIKit
-protocol InputBoxDelegate: AnyObject {
- func inputBoxDidTapAddAttachment()
- func inputBoxDidTapTool()
- func inputBoxDidTapNetwork()
- func inputBoxDidTapDeepThinking()
- func inputBoxDidTapSend()
- func inputBoxTextDidChange(_ text: String)
-}
-
class InputBox: UIView {
weak var delegate: InputBoxDelegate?
+
private lazy var containerView = UIView().then {
$0.backgroundColor = UIColor.affineLayerBackgroundPrimary
$0.layer.cornerRadius = 12
$0.layer.borderWidth = 0.5
$0.layer.borderColor = UIColor.affineLayerBorder.cgColor
+
$0.layer.shadowColor = UIColor.black.cgColor
- $0.layer.shadowOffset = CGSize(width: 0, height: 2)
- $0.layer.shadowRadius = 6
- $0.layer.shadowOpacity = 0.04
+ $0.layer.shadowOffset = CGSize(width: 0, height: 0)
+ $0.layer.shadowRadius = 12
+ $0.layer.shadowOpacity = 0.075
$0.clipsToBounds = false
}
- private lazy var textView = UITextView().then {
+ lazy var textView = UITextView().then {
$0.backgroundColor = .clear
$0.font = .systemFont(ofSize: 16)
$0.textColor = .label
@@ -34,10 +28,10 @@ class InputBox: UIView {
$0.textContainer.lineFragmentPadding = 0
$0.textContainerInset = .zero
$0.delegate = self
- $0.text = "This is AFFiNE AI"
+ $0.text = ""
}
- private lazy var placeholderLabel = UILabel().then {
+ lazy var placeholderLabel = UILabel().then {
$0.text = "Write your message..."
$0.font = .systemFont(ofSize: 16)
$0.textColor = .systemGray3
@@ -100,28 +94,27 @@ class InputBox: UIView {
$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)
+ lazy var imageBar = InputBoxImageBar().then {
+ $0.imageBarDelegate = self
}
- private lazy var mainStackView = UIStackView().then {
+ lazy var mainStackView = UIStackView().then {
$0.axis = .vertical
$0.spacing = 16
$0.alignment = .fill
+ $0.addArrangedSubview(imageBar)
$0.addArrangedSubview(textView)
- $0.addArrangedSubview(functionsStackView)
+ $0.addArrangedSubview(functionBar)
}
+
private var textViewHeightConstraint: Constraint?
private let minTextViewHeight: CGFloat = 48
private let maxTextViewHeight: CGFloat = 140
+
var text: String {
get { textView.text ?? "" }
@@ -132,26 +125,15 @@ class InputBox: UIView {
}
}
- init() {
- super.init(frame: .zero)
- setupViews()
- setupConstraints()
- updatePlaceholderVisibility()
- }
+ override init(frame: CGRect = .zero) {
+ super.init(frame: frame)
- @available(*, unavailable)
- required init?(coder _: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func setupViews() {
backgroundColor = .clear
addSubview(containerView)
containerView.addSubview(mainStackView)
containerView.addSubview(placeholderLabel)
- }
+ imageBar.isHidden = true
- private func setupConstraints() {
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(8)
}
@@ -160,18 +142,8 @@ class InputBox: UIView {
make.edges.equalToSuperview().inset(8)
}
- 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)
+ imageBar.snp.makeConstraints { make in
+ make.left.right.equalToSuperview()
}
textView.snp.makeConstraints { make in
@@ -182,58 +154,177 @@ class InputBox: UIView {
make.left.right.equalTo(textView)
make.top.equalTo(textView)
}
+
+ setupBindings()
+ updatePlaceholderVisibility()
}
- private func updateTextViewHeight() {
+ @available(*, unavailable)
+ required init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func setupBindings() {
+ // 绑定 ViewModel 到 UI
+ viewModel.$inputText
+ .removeDuplicates()
+ .sink { [weak self] text in
+ if self?.textView.text != text {
+ self?.textView.text = text
+ self?.updatePlaceholderVisibility()
+ self?.updateTextViewHeight()
+ }
+ }
+ .store(in: &cancellables)
+
+ viewModel.$isToolEnabled
+ .removeDuplicates()
+ .sink { [weak self] enabled in
+ self?.functionBar.updateToolState(isEnabled: enabled)
+ }
+ .store(in: &cancellables)
+
+ viewModel.$isNetworkEnabled
+ .removeDuplicates()
+ .sink { [weak self] enabled in
+ self?.functionBar.updateNetworkState(isEnabled: enabled)
+ }
+ .store(in: &cancellables)
+
+ viewModel.$isDeepThinkingEnabled
+ .removeDuplicates()
+ .sink { [weak self] enabled in
+ self?.functionBar.updateDeepThinkingState(isEnabled: enabled)
+ }
+ .store(in: &cancellables)
+
+ viewModel.$canSend
+ .removeDuplicates()
+ .sink { [weak self] canSend in
+ self?.functionBar.updateSendState(canSend: canSend)
+ }
+ .store(in: &cancellables)
+
+ viewModel.$hasAttachments
+ .dropFirst() // for view setup
+ .removeDuplicates()
+ .sink { [weak self] hasAttachments in
+ performWithAnimation {
+ self?.updateImageBarVisibility(hasAttachments)
+ self?.layoutIfNeeded()
+ }
+ }
+ .store(in: &cancellables)
+
+ viewModel.$attachments
+ .removeDuplicates()
+ .sink { [weak self] attachments in
+ self?.updateImageBarContent(attachments)
+ }
+ .store(in: &cancellables)
+ }
+
+ func updateTextViewHeight() {
let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude))
let newHeight = max(minTextViewHeight, min(maxTextViewHeight, size.height))
+ let height = textView.frame.height
+ guard height != newHeight else { return }
+
textViewHeightConstraint?.update(offset: newHeight)
textView.isScrollEnabled = size.height > maxTextViewHeight
- UIView.animate(
- withDuration: 0.5,
- delay: 0,
- usingSpringWithDamping: 0.8,
- initialSpringVelocity: 1.0,
- options: [.curveEaseInOut]
- ) {
+ if height == 0 || superview == nil || window == nil || isHidden { return }
+
+ performWithAnimation {
self.layoutIfNeeded()
self.superview?.layoutIfNeeded()
}
}
- private func updatePlaceholderVisibility() {
+ func updatePlaceholderVisibility() {
placeholderLabel.isHidden = !textView.text.isEmpty
}
- @objc private func addButtonTapped() {
- delegate?.inputBoxDidTapAddAttachment()
+ func updateImageBarVisibility(_ hasAttachments: Bool) {
+ imageBar.isHidden = !hasAttachments
}
- @objc private func toolButtonTapped() {
- delegate?.inputBoxDidTapTool()
+ func updateImageBarContent(_ attachments: [InputAttachment]) {
+ imageBar.updateImageBarContent(attachments)
}
- @objc private func webButtonTapped() {
- delegate?.inputBoxDidTapNetwork()
+ // 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)
+ )
+
+ performWithAnimation { [self] in
+ viewModel.addAttachment(attachment)
+ layoutIfNeeded()
+ }
}
- @objc private func reactButtonTapped() {
- delegate?.inputBoxDidTapDeepThinking()
+ public func addFileAttachment(_ url: URL) {
+ guard let fileData = try? Data(contentsOf: url) else { return }
+
+ let attachment = InputAttachment(
+ type: .file,
+ data: fileData,
+ name: url.lastPathComponent,
+ size: Int64(fileData.count)
+ )
+
+ performWithAnimation { [self] in
+ viewModel.addAttachment(attachment)
+ layoutIfNeeded()
+ }
}
- @objc private func sendButtonTapped() {
- delegate?.inputBoxDidTapSend()
+ public var inputBoxData: InputBoxData {
+ viewModel.prepareSendData()
}
}
-// MARK: - UITextViewDelegate
+// MARK: - InputBoxFunctionBarDelegate
-extension InputBox: UITextViewDelegate {
- func textViewDidChange(_ textView: UITextView) {
- updatePlaceholderVisibility()
- updateTextViewHeight()
- delegate?.inputBoxTextDidChange(textView.text)
+extension InputBox: InputBoxFunctionBarDelegate {
+ func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) {
+ delegate?.inputBoxDidSelectTakePhoto(self)
+ }
+
+ func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) {
+ delegate?.inputBoxDidSelectPhotoLibrary(self)
+ }
+
+ func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) {
+ delegate?.inputBoxDidSelectAttachFiles(self)
+ }
+
+ func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) {
+ delegate?.inputBoxDidSelectEmbedDocs(self)
+ }
+
+ func functionBarDidTapTool(_: InputBoxFunctionBar) {
+ viewModel.toggleTool()
+ }
+
+ func functionBarDidTapNetwork(_: InputBoxFunctionBar) {
+ viewModel.toggleNetwork()
+ }
+
+ func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) {
+ viewModel.toggleDeepThinking()
+ }
+
+ func functionBarDidTapSend(_: InputBoxFunctionBar) {
+ delegate?.inputBoxDidSend(self)
}
}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json
new file mode 100644
index 0000000000..0cba25aa67
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "255",
+ "green" : "255",
+ "red" : "255"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "32",
+ "green" : "32",
+ "red" : "32"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift
new file mode 100644
index 0000000000..b894949139
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift
@@ -0,0 +1,35 @@
+//
+// InputBoxDelegate.swift
+// Intelligents
+//
+// Created by 秋星桥 on 6/18/25.
+//
+
+import UIKit
+
+protocol InputBoxDelegate: AnyObject {
+ func inputBoxDidSelectTakePhoto(_ inputBox: InputBox)
+ func inputBoxDidSelectPhotoLibrary(_ inputBox: InputBox)
+ func inputBoxDidSelectAttachFiles(_ inputBox: InputBox)
+ func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox)
+ func inputBoxDidSend(_ inputBox: InputBox)
+ func inputBoxTextDidChange(_ text: String)
+}
+
+extension InputBox: InputBoxImageBarDelegate {
+ func inputBoxImageBar(_: InputBoxImageBar, didRemoveImageWithId id: InputAttachment.ID) {
+ performWithAnimation { [self] in
+ viewModel.removeAttachment(withId: id)
+ layoutIfNeeded()
+ }
+ }
+}
+
+extension InputBox: UITextViewDelegate {
+ func textViewDidChange(_ textView: UITextView) {
+ viewModel.updateText(textView.text ?? "")
+ delegate?.inputBoxTextDidChange(textView.text ?? "")
+ updatePlaceholderVisibility()
+ updateTextViewHeight()
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift
new file mode 100644
index 0000000000..4fd5e3b38a
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift
@@ -0,0 +1,196 @@
+import SnapKit
+import Then
+import UIKit
+
+protocol InputBoxFunctionBarDelegate: AnyObject {
+ func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar)
+ func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar)
+}
+
+private let unselectedColor: UIColor = .secondaryLabel
+private let selectedColor: UIColor = .accent
+
+class InputBoxFunctionBar: UIView {
+ weak var delegate: InputBoxFunctionBarDelegate?
+
+ lazy var attachmentButton = UIButton(type: .system).then {
+ $0.setImage(UIImage(named: "inputbox.add.attachment", in: .module, with: nil), for: .normal)
+ $0.tintColor = unselectedColor
+ $0.imageView?.contentMode = .scaleAspectFit
+ $0.showsMenuAsPrimaryAction = true
+ $0.menu = createAttachmentMenu()
+ }
+
+ lazy var toolButton = UIButton(type: .system).then {
+ $0.setImage(UIImage(named: "inputbox.tool", in: .module, with: nil), for: .normal)
+ $0.tintColor = unselectedColor
+ $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 = unselectedColor
+ $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 = unselectedColor
+ $0.imageView?.contentMode = .scaleAspectFit
+ $0.addTarget(self, action: #selector(deepThinkingButtonTapped), for: .touchUpInside)
+ }
+
+ lazy var sendButton = UIButton(type: .system).then {
+ $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
+ for button in [toolButton, networkButton, deepThinkingButton] {
+ button.layer.cornerRadius = button.bounds.height / 2
+ }
+ }
+
+ // MARK: - Public Methods
+
+ func updateToolState(isEnabled: Bool) {
+ toolButton.tintColor = isEnabled ? selectedColor : unselectedColor
+ }
+
+ func updateNetworkState(isEnabled: Bool) {
+ networkButton.tintColor = isEnabled ? selectedColor : unselectedColor
+ }
+
+ func updateDeepThinkingState(isEnabled: Bool) {
+ deepThinkingButton.tintColor = isEnabled ? selectedColor : unselectedColor
+ }
+
+ func updateSendState(canSend: Bool) {
+ sendButton.isEnabled = canSend
+ sendButton.alpha = canSend ? 1.0 : 0.5
+ }
+
+ // MARK: - Private Methods
+
+ private func createAttachmentMenu() -> UIMenu {
+ let takePhotoAction = UIAction(
+ title: "Take Photo or Video",
+ image: UIImage(systemName: "camera")
+ ) { [weak self] _ in
+ guard let self else { return }
+ delegate?.functionBarDidTapTakePhoto(self)
+ }
+
+ let photoLibraryAction = UIAction(
+ title: "Photo Library",
+ image: UIImage(systemName: "photo.on.rectangle")
+ ) { [weak self] _ in
+ guard let self else { return }
+ delegate?.functionBarDidTapPhotoLibrary(self)
+ }
+
+ let attachFilesAction = UIAction(
+ title: "Attach Files (pdf, txt, csv)",
+ image: UIImage(systemName: "arrow.up.doc")
+ ) { [weak self] _ in
+ guard let self else { return }
+ delegate?.functionBarDidTapAttachFiles(self)
+ }
+
+ let embedDocsAction = UIAction(
+ title: "Embed AFFINE Docs",
+ image: UIImage(systemName: "doc.text")
+ ) { [weak self] _ in
+ guard let self else { return }
+ delegate?.functionBarDidTapEmbedDocs(self)
+ }
+
+ return UIMenu(
+ options: [.displayInline],
+ children: [takePhotoAction, photoLibraryAction, attachFilesAction, embedDocsAction].reversed()
+ )
+ }
+
+ // MARK: - Actions
+
+ @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)
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift
new file mode 100644
index 0000000000..352e272bad
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift
@@ -0,0 +1,174 @@
+//
+// 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)
+ 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: 0, y: 0, width: constantHeight, height: constantHeight),
+ image: image,
+ attachmentId: attachment.id
+ )
+ imageCell.onRemove = { [weak self] cell in
+ self?.removeImageCell(cell)
+ }
+
+ let viewModel = AttachmentViewModel(attachment: attachment, imageCell: imageCell)
+ attachmentViewModels.append(viewModel)
+ addSubview(imageCell)
+ }
+ }
+
+ layoutImageCells()
+ }
+
+ func removeImageCell(_ cell: ImageCell) {
+ if let index = attachmentViewModels.firstIndex(where: { $0.imageCell === cell }) {
+ let viewModel = attachmentViewModels.remove(at: index)
+ viewModel.imageCell.removeFromSuperviewWithExplodeEffect()
+ imageBarDelegate?.inputBoxImageBar(self, didRemoveImageWithId: cell.attachmentId)
+ layoutImageCells()
+ }
+ }
+
+ func clear() {
+ for viewModel in attachmentViewModels {
+ viewModel.imageCell.removeFromSuperview()
+ }
+ attachmentViewModels.removeAll()
+ contentSize = .zero
+ }
+
+ private func layoutImageCells() {
+ var xOffset: CGFloat = 0
+
+ for viewModel in attachmentViewModels {
+ viewModel.imageCell.frame = CGRect(x: xOffset, y: 0, width: constantHeight, height: constantHeight)
+ xOffset += constantHeight + cellSpacing
+ }
+
+ // Update content size
+ let totalWidth = max(0, xOffset - cellSpacing)
+ contentSize = CGSize(width: totalWidth, height: constantHeight)
+ }
+}
+
+extension InputBoxImageBar {
+ class ImageCell: UIView {
+ let attachmentId: UUID
+ var onRemove: ((ImageCell) -> Void)?
+
+ private lazy var imageView = UIImageView(frame: bounds).then {
+ $0.contentMode = .scaleAspectFill
+ $0.clipsToBounds = true
+ $0.layer.cornerRadius = 12
+ $0.layer.cornerCurve = .continuous
+ $0.backgroundColor = .systemGray6
+ }
+
+ private lazy var removeButton = DeleteButtonView(frame: removeButtonFrame).then {
+ $0.onTapped = { [weak self] in
+ self?.removeButtonTapped()
+ }
+ }
+
+ init(frame: CGRect, image: UIImage, attachmentId: UUID) {
+ self.attachmentId = attachmentId
+ super.init(frame: frame)
+ addSubview(imageView)
+ addSubview(removeButton)
+ imageView.image = image
+ }
+
+ var removeButtonFrame: CGRect {
+ let buttonSize: CGFloat = 18
+ let buttonInset: CGFloat = 6
+ return CGRect(
+ x: bounds.width - buttonSize - buttonInset,
+ y: buttonInset,
+ width: buttonSize,
+ height: buttonSize
+ )
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ imageView.frame = bounds
+
+ removeButton.frame = removeButtonFrame
+ }
+
+ @available(*, unavailable)
+ required init?(coder _: NSCoder) {
+ fatalError()
+ }
+
+ @objc private func removeButtonTapped() {
+ onRemove?(self)
+ }
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift
new file mode 100644
index 0000000000..9a3a249122
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift
@@ -0,0 +1,164 @@
+//
+// InputBoxViewModel.swift
+// Intelligents
+//
+// Created by AI Assistant on 6/17/25.
+//
+
+import Combine
+import Foundation
+
+// MARK: - Data Models
+
+public struct InputAttachment: Identifiable, Equatable, Hashable, Codable {
+ public var id: UUID = .init()
+ public var type: AttachmentType
+ public var data: Data?
+ public var url: URL?
+ public var name: String
+ public var size: Int64
+
+ public enum AttachmentType: String, Equatable, Hashable, Codable {
+ case image
+ case document
+ case file
+ }
+
+ public init(
+ type: AttachmentType,
+ data: Data? = nil,
+ url: URL? = nil,
+ name: String,
+ size: Int64 = 0
+ ) {
+ self.type = type
+ self.data = data
+ self.url = url
+ self.name = name
+ self.size = size
+ }
+}
+
+public struct InputBoxData {
+ public var text: String
+ public var attachments: [InputAttachment]
+ public var isToolEnabled: Bool
+ public var isNetworkEnabled: Bool
+ public var isDeepThinkingEnabled: Bool
+
+ public init(
+ text: String,
+ attachments: [InputAttachment],
+ isToolEnabled: Bool,
+ isNetworkEnabled: Bool,
+ isDeepThinkingEnabled: Bool
+ ) {
+ self.text = text
+ self.attachments = attachments
+ self.isToolEnabled = isToolEnabled
+ self.isNetworkEnabled = isNetworkEnabled
+ self.isDeepThinkingEnabled = isDeepThinkingEnabled
+ }
+}
+
+// MARK: - View Model
+
+public class InputBoxViewModel: ObservableObject {
+ // MARK: - Published Properties
+
+ @Published public var inputText: String = ""
+ @Published public var isToolEnabled: Bool = false
+ @Published public var isNetworkEnabled: Bool = false
+ @Published public var isDeepThinkingEnabled: Bool = false
+ @Published public var hasAttachments: Bool = false
+ @Published public var attachments: [InputAttachment] = []
+ @Published public var canSend: Bool = false
+
+ // MARK: - Private Properties
+
+ private var cancellables = Set()
+
+ // MARK: - Initialization
+
+ public init() {
+ setupBindings()
+ }
+
+ // MARK: - Private Methods
+
+ private func setupBindings() {
+ // 监听文本变化,自动更新发送按钮状态
+ $inputText
+ .map { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
+ .assign(to: \.canSend, on: self)
+ .store(in: &cancellables)
+
+ // 监听附件变化
+ $attachments
+ .map { !$0.isEmpty }
+ .assign(to: \.hasAttachments, on: self)
+ .store(in: &cancellables)
+ }
+}
+
+// MARK: - Text Management
+
+public extension InputBoxViewModel {
+ func updateText(_ text: String) {
+ inputText = text
+ }
+}
+
+// MARK: - Feature Toggles
+
+public extension InputBoxViewModel {
+ func toggleTool() {
+ isToolEnabled.toggle()
+ }
+
+ func toggleNetwork() {
+ isNetworkEnabled.toggle()
+ }
+
+ func toggleDeepThinking() {
+ isDeepThinkingEnabled.toggle()
+ }
+}
+
+// MARK: - Attachment Management
+
+public extension InputBoxViewModel {
+ func addAttachment(_ attachment: InputAttachment) {
+ attachments.append(attachment)
+ }
+
+ func removeAttachment(withId id: UUID) {
+ attachments.removeAll { $0.id == id }
+ }
+
+ func clearAttachments() {
+ attachments.removeAll()
+ }
+}
+
+// MARK: - Send Management
+
+public extension InputBoxViewModel {
+ func prepareSendData() -> InputBoxData {
+ InputBoxData(
+ text: inputText.trimmingCharacters(in: .whitespacesAndNewlines),
+ attachments: attachments,
+ isToolEnabled: isToolEnabled,
+ isNetworkEnabled: isNetworkEnabled,
+ isDeepThinkingEnabled: isDeepThinkingEnabled
+ )
+ }
+
+ func resetInput() {
+ inputText = ""
+ attachments.removeAll()
+ isToolEnabled = false
+ isNetworkEnabled = false
+ isDeepThinkingEnabled = false
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift
index 9dae17c6c3..d389a056bc 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift
@@ -48,12 +48,7 @@ public extension UIViewController {
button.stopProgress()
view.layoutIfNeeded()
- UIView.animate(
- withDuration: 0.5,
- delay: 0,
- usingSpringWithDamping: 1.0,
- initialSpringVelocity: 0.8
- ) {
+ performWithAnimation {
button.alpha = 1
button.transform = .identity
button.setNeedsLayout()
@@ -78,12 +73,7 @@ public extension UIViewController {
button.stopProgress()
button.setNeedsLayout()
view.layoutIfNeeded()
- UIView.animate(
- withDuration: 0.5,
- delay: 0,
- usingSpringWithDamping: 1.0,
- initialSpringVelocity: 0.8
- ) {
+ performWithAnimation {
button.alpha = 0
button.transform = .init(scaleX: 0, y: 0)
button.setNeedsLayout()
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift
index 59c1c32d01..a633a5c12e 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift
@@ -6,6 +6,7 @@
//
import SnapKit
+import SwifterSwift
import Then
import UIKit
@@ -17,7 +18,10 @@ public class IntelligentsButton: UIView {
}
lazy var background = UIView().then {
- $0.backgroundColor = .white
+ $0.backgroundColor = .init(
+ light: .systemBackground,
+ dark: .darkGray.withAlphaComponent(0.25)
+ )
}
lazy var activityIndicator = UIActivityIndicatorView()
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift
index a7ace43754..05a74cd8f8 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift
@@ -68,7 +68,9 @@ class MainHeaderView: UIView {
init() {
super.init(frame: .zero)
+
backgroundColor = UIColor.clear
+
addSubview(mainStackView)
mainStackView.snp.makeConstraints { make in
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift
new file mode 100644
index 0000000000..33fbf1f290
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift
@@ -0,0 +1,56 @@
+import UIKit
+
+class DeleteButtonView: UIView {
+ let imageView = UIImageView(image: .init(systemName: "xmark")).then {
+ $0.tintColor = .white
+ $0.contentMode = .scaleAspectFit
+ }
+
+ let blur = UIVisualEffectView(
+ effect: UIBlurEffect(style: .systemUltraThinMaterialDark)
+ ).then {
+ $0.clipsToBounds = true
+ }
+
+ var onTapped: () -> Void = {}
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ isUserInteractionEnabled = true
+
+ addSubview(blur)
+ addSubview(imageView)
+
+ blur.snp.makeConstraints { make in
+ make.edges.equalToSuperview()
+ }
+ imageView.snp.makeConstraints { make in
+ make.edges.equalToSuperview().inset(2)
+ }
+
+ let gesture = UITapGestureRecognizer(target: self, action: #selector(tapped))
+ addGestureRecognizer(gesture)
+ }
+
+ @available(*, unavailable)
+ required init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
+ if bounds.width < 50 || bounds.height < 50 {
+ return bounds.insetBy(dx: -20, dy: -20).contains(point)
+ }
+ return super.point(inside: point, with: event)
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ blur.layer.cornerRadius = min(bounds.width, bounds.height) / 2
+ }
+
+ @objc func tapped() {
+ onTapped()
+ }
+}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift
index 369c707c1b..605ff9261e 100644
--- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift
@@ -5,6 +5,7 @@
// Created by 秋星桥 on 6/17/25.
//
+import Combine
import Foundation
import WebKit
@@ -14,11 +15,30 @@ public class IntelligentContext {
public var webView: WKWebView!
+ public lazy var temporaryDirectory: URL = {
+ let tempDir = FileManager.default.temporaryDirectory
+ return tempDir.appendingPathComponent("IntelligentContext")
+ }()
+
private init() {}
public func preparePresent(_ completion: @escaping () -> Void) {
- // used to gathering information, populate content from webview, etc.
- // TODO: if needed
- completion()
+ DispatchQueue.global(qos: .userInitiated).async { [self] in
+ prepareTemporaryDirectory()
+ // TODO: used to gathering information, populate content from webview, etc.
+ DispatchQueue.main.async {
+ completion()
+ }
+ }
+ }
+
+ func prepareTemporaryDirectory() {
+ if FileManager.default.fileExists(atPath: temporaryDirectory.path) {
+ try? FileManager.default.removeItem(at: temporaryDirectory)
+ }
+ try? FileManager.default.createDirectory(
+ at: temporaryDirectory,
+ withIntermediateDirectories: true
+ )
}
}
diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/main.metal b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/main.metal
new file mode 100644
index 0000000000..d79a65f82d
--- /dev/null
+++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/main.metal
@@ -0,0 +1,87 @@
+#include
+
+using namespace metal;
+
+namespace ParticleTransitionSystem {
+
+ struct TrollParticle {
+ float2 position;
+ float2 velocity;
+ float life;
+ float duration;
+ };
+
+ struct TrollVertex {
+ float4 position [[position]];
+ float2 uv;
+ float opacity;
+ };
+
+}
+
+// 顶点着色器 负责将粒子数据转换为顶点数据
+vertex ParticleTransitionSystem::TrollVertex PTS_ParticleVertex(const device ParticleTransitionSystem::TrollVertex *vertices [[buffer(0)]],
+ const device float2 &resolution [[buffer(1)]],
+ const device ParticleTransitionSystem::TrollParticle *particles [[buffer(2)]],
+ const device float2 &targetFrameSize [[buffer(3)]],
+ const device float &stepSize [[buffer(4)]],
+ unsigned int vid [[vertex_id]],
+ unsigned int particleId [[instance_id]]) {
+ ParticleTransitionSystem::TrollVertex v = vertices[vid];
+ ParticleTransitionSystem::TrollParticle p = particles[particleId];
+
+ int particlesPerRow = int(targetFrameSize.x / stepSize);
+ int row = particleId / particlesPerRow;
+ int col = particleId % particlesPerRow;
+
+ float2 originalPos = float2(col * stepSize + stepSize / 2.0, row * stepSize + stepSize / 2.0);
+ float2 currentPos = p.position;
+
+ // 计算目标帧在屏幕中的居中偏移
+ float2 offset = (resolution - targetFrameSize) / 2.0;
+
+ // 设置UV坐标用于纹理采样
+ v.uv.x = originalPos.x / targetFrameSize.x;
+ v.uv.y = originalPos.y / targetFrameSize.y;
+
+ // 计算最终的屏幕位置
+ float particleSize = stepSize;
+ float2 worldPos = v.position.xy * particleSize + currentPos + offset;
+
+ // 转换到NDC坐标系 (-1到1)
+ v.position.x = (worldPos.x / resolution.x) * 2.0 - 1.0;
+ v.position.y = 1.0 - (worldPos.y / resolution.y) * 2.0;
+
+ // 逐渐消失
+ v.opacity = p.life / p.duration;
+
+ return v;
+}
+
+// 片段着色器 负责将顶点数据转换为像素颜色
+fragment float4 PTS_ParticleFragment(ParticleTransitionSystem::TrollVertex in [[stage_in]],
+ const texture2d texture [[texture(0)]],
+ const sampler textureSampler [[sampler(0)]]) {
+ constexpr sampler samplr;
+ float4 color = texture.sample(samplr, in.uv);
+ float a = color.a * in.opacity; // apply texture alpha
+ color *= in.opacity; // apply opacity from vertex shader aka pre-multiplied alpha
+ color.a = a;
+ return color;
+}
+
+// 计算粒子位置和速度的计算着色器 负责更新粒子的位置和速度
+kernel void PTS_UpdateParticles(device ParticleTransitionSystem::TrollParticle *particles [[buffer(0)]],
+ unsigned int index [[thread_position_in_grid]]) {
+ if (particles[index].life >= 0) {
+ particles[index].position += particles[index].velocity;
+
+ // 模拟空气阻力,降低速度 x, y 分量
+ particles[index].velocity.x *= 0.99;
+ particles[index].velocity.y *= 0.99;
+
+ // 模拟重力影响,增加 y 分量
+ particles[index].velocity.y += 0.1;
+ }
+ particles[index].life -= 1.0;
+}