Skip to content

Commit f9f487f

Browse files
authored
Merge pull request #15 from maiqingqiang/Apple-Intelligence-UI
✨ Apple Intelligence Effect
2 parents 5d80ed0 + debdd8d commit f9f487f

18 files changed

Lines changed: 573 additions & 20 deletions

ChatMLX.xcodeproj/project.pbxproj

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@
8484
52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */; };
8585
52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */; };
8686
52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */; };
87+
52B053932CB2B0D500E8DDBA /* ColorWheel.metal in Sources */ = {isa = PBXBuildFile; fileRef = 52B053922CB2B0D300E8DDBA /* ColorWheel.metal */; };
88+
52B053952CB2B64500E8DDBA /* NoneInteractWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */; };
89+
52B0539D2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */; };
90+
52B0539E2CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */; };
91+
52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */; };
92+
52B053A22CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */; };
93+
52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */; };
8794
52E50B1D2C8D6E81005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1C2C8D6E81005A89DE /* LLM */; };
8895
52E50B202C8D719B005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1F2C8D719B005A89DE /* LLM */; };
8996
52E50B222C8D719B005A89DE /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B212C8D719B005A89DE /* MNIST */; };
@@ -166,6 +173,13 @@
166173
52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = "<group>"; };
167174
52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
168175
52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertModifier.swift; sourceTree = "<group>"; };
176+
52B053922CB2B0D300E8DDBA /* ColorWheel.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ColorWheel.metal; sourceTree = "<group>"; };
177+
52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoneInteractWindow.swift; sourceTree = "<group>"; };
178+
52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectController.swift; sourceTree = "<group>"; };
179+
52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectView.swift; sourceTree = "<group>"; };
180+
52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectManager.swift; sourceTree = "<group>"; };
181+
52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectDisplay.swift; sourceTree = "<group>"; };
182+
52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = "<group>"; };
169183
/* End PBXFileReference section */
170184

171185
/* Begin PBXFrameworksBuildPhase section */
@@ -256,6 +270,8 @@
256270
526676092C85F903001EF113 /* Components */ = {
257271
isa = PBXGroup;
258272
children = (
273+
52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */,
274+
52B0538B2CB2B03200E8DDBA /* AppleIntelligenceEffect */,
259275
52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */,
260276
526676002C85F903001EF113 /* SyntaxHighlighter */,
261277
526676012C85F903001EF113 /* EffectView.swift */,
@@ -315,6 +331,7 @@
315331
526676222C85F903001EF113 /* Settings */ = {
316332
isa = PBXGroup;
317333
children = (
334+
52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */,
318335
52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */,
319336
526676142C85F903001EF113 /* DownloadManager */,
320337
526676172C85F903001EF113 /* LocalModels */,
@@ -345,6 +362,7 @@
345362
526676252C85F903001EF113 /* DisplayStyle.swift */,
346363
526676262C85F903001EF113 /* DownloadTask.swift */,
347364
526676272C85F903001EF113 /* Language.swift */,
365+
52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */,
348366
526676282C85F903001EF113 /* LocalModel.swift */,
349367
526676292C85F903001EF113 /* LocalModelGroup.swift */,
350368
528D83382CAE51EC00163AAB /* Role.swift */,
@@ -396,6 +414,17 @@
396414
path = Extensions;
397415
sourceTree = "<group>";
398416
};
417+
52B0538B2CB2B03200E8DDBA /* AppleIntelligenceEffect */ = {
418+
isa = PBXGroup;
419+
children = (
420+
52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */,
421+
52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */,
422+
52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */,
423+
52B053922CB2B0D300E8DDBA /* ColorWheel.metal */,
424+
);
425+
path = AppleIntelligenceEffect;
426+
sourceTree = "<group>";
427+
};
399428
/* End PBXGroup section */
400429

401430
/* Begin PBXNativeTarget section */
@@ -510,11 +539,14 @@
510539
files = (
511540
526676422C85F903001EF113 /* TextOutputFormat.swift in Sources */,
512541
526676512C85F903001EF113 /* RightSidebarView.swift in Sources */,
542+
52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */,
513543
526676682C85F903001EF113 /* SettingsTabGroup.swift in Sources */,
514544
526676452C85F903001EF113 /* UltramanNavigationSplitView.swift in Sources */,
515545
526676672C85F903001EF113 /* SettingsTab.swift in Sources */,
546+
52B053932CB2B0D500E8DDBA /* ColorWheel.metal in Sources */,
516547
5266765C2C85F903001EF113 /* SettingsSidebarItemView.swift in Sources */,
517548
526676592C85F903001EF113 /* DefaultConversationView.swift in Sources */,
549+
52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */,
518550
5266766E2C85F903001EF113 /* Logger.swift in Sources */,
519551
526676612C85F903001EF113 /* DownloadTask.swift in Sources */,
520552
52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */,
@@ -544,6 +576,7 @@
544576
528D832A2CAD5C9100163AAB /* Conversation+CoreDataProperties.swift in Sources */,
545577
528D832B2CAD5C9100163AAB /* Message+CoreDataClass.swift in Sources */,
546578
528D83392CAE51EC00163AAB /* Role.swift in Sources */,
579+
52B053952CB2B64500E8DDBA /* NoneInteractWindow.swift in Sources */,
547580
528D832C2CAD5C9100163AAB /* Message+CoreDataProperties.swift in Sources */,
548581
526676712C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift in Sources */,
549582
526676532C85F903001EF113 /* DownloadTaskView.swift in Sources */,
@@ -554,6 +587,8 @@
554587
526676412C85F903001EF113 /* SplashCodeSyntaxHighlighter.swift in Sources */,
555588
526676542C85F903001EF113 /* LocalModelItemView.swift in Sources */,
556589
526676632C85F903001EF113 /* LocalModel.swift in Sources */,
590+
52B0539D2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift in Sources */,
591+
52B0539E2CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift in Sources */,
557592
526676692C85F903001EF113 /* Styles.swift in Sources */,
558593
5266765A2C85F903001EF113 /* GeneralView.swift in Sources */,
559594
526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */,
@@ -565,6 +600,7 @@
565600
528D83372CADB64600163AAB /* ConversationViewModel.swift in Sources */,
566601
5266765D2C85F903001EF113 /* SettingsSidebarView.swift in Sources */,
567602
5266764B2C85F903001EF113 /* ConversationDetailView.swift in Sources */,
603+
52B053A22CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift in Sources */,
568604
526676602C85F903001EF113 /* DisplayStyle.swift in Sources */,
569605
5266765B2C85F903001EF113 /* HuggingFaceView.swift in Sources */,
570606
526676642C85F903001EF113 /* LocalModelGroup.swift in Sources */,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// FireworkController.swift
3+
// Firework
4+
//
5+
// Created by 秋星桥 on 2024/2/7.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
class AppleIntelligenceEffectController: NSWindowController {
12+
override init(window: NSWindow?) {
13+
super.init(window: window)
14+
contentViewController = AppleIntelligenceEffectViewController()
15+
}
16+
17+
@available(*, unavailable)
18+
required init?(coder _: NSCoder) { fatalError() }
19+
20+
convenience init(screen: NSScreen) {
21+
let window = NoneInteractWindow(
22+
contentRect: screen.frame,
23+
styleMask: [.borderless, .fullSizeContentView],
24+
backing: .buffered,
25+
defer: false,
26+
screen: screen
27+
)
28+
self.init(window: window)
29+
}
30+
}
31+
32+
extension AppleIntelligenceEffectController {
33+
func configureWindow(for screen: NSScreen) {
34+
window?.setFrameOrigin(screen.frame.origin)
35+
window?.setContentSize(screen.frame.size)
36+
window?.orderFrontRegardless()
37+
}
38+
}
39+
40+
class AppleIntelligenceEffectViewController: NSViewController {
41+
override func loadView() {
42+
view = NSHostingView(rootView: AppleIntelligenceEffectView())
43+
}
44+
45+
func fadeOut(completion: (() -> Void)?) {
46+
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
47+
fadeOutAnimation.fromValue = 1.0
48+
fadeOutAnimation.toValue = 0.0
49+
fadeOutAnimation.duration = 1.0
50+
fadeOutAnimation.isRemovedOnCompletion = true
51+
view.layer?.add(fadeOutAnimation, forKey: "opacity")
52+
view.layer?.opacity = 0.0
53+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
54+
completion?()
55+
}
56+
}
57+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// AppleIntelligenceEffectManager.swift
3+
// ChatMLX
4+
//
5+
// Created by John Mai on 2024/10/6.
6+
//
7+
8+
import AppKit
9+
10+
class AppleIntelligenceEffectManager {
11+
static let shared = AppleIntelligenceEffectManager()
12+
13+
private var effectController: AppleIntelligenceEffectController?
14+
15+
private init() {}
16+
17+
func setupEffect() {
18+
guard effectController == nil, let screen = NSScreen.main else { return }
19+
effectController = AppleIntelligenceEffectController(screen: screen)
20+
effectController?.configureWindow(for: screen)
21+
}
22+
23+
func closeEffect(completion: (() -> Void)? = nil) {
24+
guard let controller = effectController else {
25+
completion?()
26+
return
27+
}
28+
29+
(controller.contentViewController as? AppleIntelligenceEffectViewController)?.fadeOut {
30+
[weak self] in
31+
controller.window?.close()
32+
self?.effectController = nil
33+
completion?()
34+
}
35+
}
36+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// AppleIntelligenceEffectView.swift
3+
// test
4+
//
5+
// Created by John Mai on 2024/10/6.
6+
//
7+
8+
import SwiftUI
9+
10+
struct AppleIntelligenceEffectView: View {
11+
private let shader = ShaderLibrary.colorWheel(.boundingRect, .float(1))
12+
private let angles = [133.0, -133.0]
13+
private let maxBlurRadiusBase: CGFloat = 18
14+
private let minBlurRadiusBase: CGFloat = 6
15+
16+
var useRoundedRectangle: Bool = true
17+
18+
var body: some View {
19+
TimelineView(.animation) { timeline in
20+
ZStack {
21+
ForEach(angles.indices, id: \.self) { index in
22+
colorWheelRectangle(for: timeline.date, angle: angles[index])
23+
}
24+
}
25+
}
26+
}
27+
28+
@MainActor
29+
private func colorWheelRectangle(for date: Date, angle: Double) -> some View {
30+
let time = date.timeIntervalSince1970
31+
let blurRadius =
32+
angle > 0
33+
? maxBlurRadiusBase + 6 * sin(time * 2)
34+
: minBlurRadiusBase + 3 * sin(time * 4)
35+
36+
return Rectangle()
37+
.fill(shader)
38+
.rotationEffect(.degrees(time * 60))
39+
.scaleEffect(2.4)
40+
.rotationEffect(.degrees(time * angle))
41+
.mask(alignment: .center) {
42+
if useRoundedRectangle {
43+
UnevenRoundedRectangle(
44+
cornerRadii: .init(
45+
topLeading: 20,
46+
bottomLeading: 0,
47+
bottomTrailing: 0,
48+
topTrailing: 20
49+
)
50+
)
51+
.stroke(lineWidth: maxBlurRadiusBase)
52+
.blur(radius: blurRadius)
53+
} else {
54+
Rectangle()
55+
.stroke(lineWidth: maxBlurRadiusBase)
56+
.blur(radius: blurRadius)
57+
}
58+
}
59+
}
60+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// ColorWheel.metal
3+
// ChatMLX
4+
//
5+
// Created by John Mai on 2024/10/6.
6+
//
7+
8+
#include <metal_stdlib>
9+
using namespace metal;
10+
11+
#define M_TWO_PI_F (M_PI_F * 2)
12+
13+
float3 hsv2rgb(float3 c) {
14+
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
15+
float3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
16+
return c.z * mix(K.xxx, saturate(p - K.xxx), c.y);
17+
}
18+
19+
[[ stitchable ]] half4 colorWheel(float2 position, float4 bounds, float brightness) {
20+
float2 center = position / bounds.zw - 0.5;
21+
float hue = (atan2(center.y, center.x) + M_PI_F) / M_TWO_PI_F;
22+
float saturation = min(length(center) * 2.0, 1.0);
23+
float value = saturate(brightness);
24+
return half4(half3(hsv2rgb(float3(hue, saturation, value))), 1.0);
25+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// NoneInteractWindow.swift
3+
// FireBox
4+
//
5+
// Created by 秋星桥 on 2024/2/9.
6+
//
7+
8+
import AppKit
9+
10+
class NoneInteractWindow: NSWindow {
11+
override init(
12+
contentRect: NSRect,
13+
styleMask: NSWindow.StyleMask,
14+
backing: NSWindow.BackingStoreType,
15+
defer flag: Bool
16+
) {
17+
super.init(
18+
contentRect: contentRect,
19+
styleMask: styleMask,
20+
backing: backing,
21+
defer: flag
22+
)
23+
24+
isOpaque = false
25+
alphaValue = 1
26+
titleVisibility = .hidden
27+
titlebarAppearsTransparent = true
28+
backgroundColor = NSColor.clear
29+
ignoresMouseEvents = true
30+
isMovable = false
31+
collectionBehavior = [
32+
.fullScreenAuxiliary,
33+
.stationary,
34+
.canJoinAllSpaces,
35+
.ignoresCycle,
36+
]
37+
level = .statusBar
38+
hasShadow = false
39+
}
40+
41+
override var canBecomeKey: Bool {
42+
false
43+
}
44+
45+
override var canBecomeMain: Bool {
46+
false
47+
}
48+
}

ChatMLX/Extensions/Defaults+Extensions.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ extension Defaults.Keys {
3636
static let defaultSystemPrompt = Key<String>("defaultSystemPrompt", default: "")
3737

3838
static let gpuCacheLimit = Key<Int32>("gpuCacheLimit", default: 128)
39+
40+
static let enableAppleIntelligenceEffect = Key<Bool>(
41+
"enableAppleIntelligenceEffect", default: false)
42+
static let appleIntelligenceEffectDisplay = Key<AppleIntelligenceEffectDisplay>(
43+
"appleIntelligenceEffectDisplay", default: .appInternal)
3944
}

ChatMLX/Features/Conversation/ConversationDetailView.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ struct ConversationDetailView: View {
3535

3636
@FocusState private var isInputFocused: Bool
3737

38+
@Default(.enableAppleIntelligenceEffect) var enableAppleIntelligenceEffect
39+
@Default(.appleIntelligenceEffectDisplay) var appleIntelligenceEffectDisplay
40+
3841
var body: some View {
3942
ZStack(alignment: .trailing) {
4043
VStack(spacing: 0) {
@@ -298,21 +301,20 @@ struct ConversationDetailView: View {
298301

299302
Message(context: viewContext).user(content: trimmedMessage, conversation: conversation)
300303

301-
runner.generate(conversation: conversation, in: viewContext) {
302-
scrollToBottom()
304+
if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global {
305+
AppleIntelligenceEffectManager.shared.setupEffect()
303306
}
304307

305-
scrollToBottom()
306-
307-
Task(priority: .background) {
308-
do {
309-
try await viewContext.perform {
310-
if viewContext.hasChanges {
311-
try viewContext.save()
312-
}
308+
runner.generate(conversation: conversation, in: viewContext) {
309+
Task { @MainActor in
310+
scrollToBottom()
311+
}
312+
} completion: {
313+
Task { @MainActor in
314+
if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global {
315+
AppleIntelligenceEffectManager.shared.closeEffect()
313316
}
314-
} catch {
315-
vm.throwError(error, title: "Send Message Failed")
317+
scrollToBottom()
316318
}
317319
}
318320
}

0 commit comments

Comments
 (0)