Compare commits

...

16 Commits

Author SHA1 Message Date
EYHN
9774d133e3 demo 2024-12-16 16:01:17 +08:00
砍砍
1c685fb5a4 Just Commit 2024-12-13 14:09:01 +08:00
砍砍
8333f00aec Draw the Cells 2024-12-13 13:51:51 +08:00
砍砍
2fee181633 Init Chat Logical Flow 2024-12-06 17:50:51 +08:00
砍砍
af3fa410a6 Fix Build 2024-12-06 17:42:19 +08:00
砍砍
28f8f71e0a Done Editing Box 2024-12-06 17:41:13 +08:00
砍砍
4cea298fa6 Done PhotoPicker 2024-12-06 17:13:09 +08:00
砍砍
2721815907 Input Edit + Keyboard Animation 2024-12-06 16:48:26 +08:00
砍砍
561c73f2bf WIP: 嵌入式框架优化调整 2024-11-21 22:01:13 +08:00
砍砍
d252de0c5a WIP: 嵌入式框架优化调整 2024-11-21 21:59:13 +08:00
砍砍
9d0de52609 WIP: 完成 AI 框架嵌入文档的动画 2024-11-21 19:10:03 +08:00
Lakr
e68070186a Merge pull request #8864 from toeverything/eyhn/ios-get-content-in-markdown-api
feat(mobile): get content in markdown api
2024-11-21 15:40:38 +08:00
EYHN
b28b555d80 feat(mobile): get content in markdown api 2024-11-19 14:36:52 +08:00
Lakr
442c86011a Merge pull request #8860 from toeverything/eyhn/affine-intelligent-integrated
feat(mobile): integrate ai button
2024-11-19 12:14:00 +08:00
EYHN
585fd2206c feat(mobile): integrate ai button 2024-11-19 12:12:42 +08:00
砍砍
60275046b4 WIP: 打个框架先 2024-11-19 11:15:33 +08:00
80 changed files with 3151 additions and 198 deletions

View File

@@ -66,6 +66,7 @@ export function configureWorkspaceModule(framework: Framework) {
.service(WorkspaceRepositoryService, [
[WorkspaceFlavourProvider],
WorkspaceProfileService,
WorkspaceListService,
])
.scope(WorkspaceScope)
.service(WorkspaceService)

View File

@@ -11,6 +11,7 @@ import type {
WorkspaceFlavourProvider,
} from '../providers/flavour';
import { WorkspaceScope } from '../scopes/workspace';
import type { WorkspaceListService } from './list';
import type { WorkspaceProfileService } from './profile';
import { WorkspaceService } from './workspace';
@@ -19,7 +20,8 @@ const logger = new DebugLogger('affine:workspace-repository');
export class WorkspaceRepositoryService extends Service {
constructor(
private readonly providers: WorkspaceFlavourProvider[],
private readonly profileRepo: WorkspaceProfileService
private readonly profileRepo: WorkspaceProfileService,
private readonly workspacesListService: WorkspaceListService
) {
super();
}
@@ -77,6 +79,12 @@ export class WorkspaceRepositoryService extends Service {
};
};
openByWorkspaceId = (workspaceId: string) => {
const workspaceMetadata =
this.workspacesListService.list.workspace$(workspaceId).value;
return workspaceMetadata && this.open({ metadata: workspaceMetadata });
};
instantiate(
openOptions: WorkspaceOpenOptions,
customProvider?: WorkspaceEngineProvider

View File

@@ -41,6 +41,10 @@ export class WorkspacesService extends Service {
return this.workspaceRepo.open;
}
get openByWorkspaceId() {
return this.workspaceRepo.openByWorkspaceId;
}
get create() {
return this.workspaceFactory.create;
}

View File

@@ -137,12 +137,9 @@ events?.applicationMenu.onNewPageAction(() => {
.get(GlobalContextService)
.globalContext.workspaceId.get();
const workspacesService = frameworkProvider.get(WorkspacesService);
const workspaceMetadata = currentWorkspaceId
? workspacesService.list.workspace$(currentWorkspaceId).value
const workspaceRef = currentWorkspaceId
? workspacesService.openByWorkspaceId(currentWorkspaceId)
: null;
const workspaceRef =
workspaceMetadata &&
workspacesService.open({ metadata: workspaceMetadata });
if (!workspaceRef) {
return;
}

View File

@@ -7,10 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
501FBC562D02F88200507774 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 501FBC552D02F88200507774 /* InfoPlist.xcstrings */; };
501FBC582D02F88800507774 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 501FBC572D02F88800507774 /* Localizable.xcstrings */; };
505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 505B0A332CEB3FB10092FC35 /* Intelligents */; };
505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505B0A352CEB48B10092FC35 /* RootViewController.swift */; };
9D1C07272CEC3E9500E1C502 /* IntelligentsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D1C07262CEC3E8200E1C502 /* IntelligentsPlugin.swift */; };
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; };
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */; };
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */; };
9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1D2CCB9876006677DB /* Assets.xcassets */; };
9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1E2CCB9876006677DB /* capacitor.config.json */; };
@@ -21,11 +26,16 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
501FBC552D02F88200507774 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
501FBC572D02F88800507774 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
505B0A312CEB3FAB0092FC35 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; };
505B0A352CEB48B10092FC35 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = "<group>"; };
9D1C07262CEC3E8200E1C502 /* IntelligentsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntelligentsPlugin.swift; sourceTree = "<group>"; };
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = "<group>"; };
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineViewController.swift; sourceTree = "<group>"; };
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9D90BE1E2CCB9876006677DB /* capacitor.config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
@@ -43,6 +53,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */,
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -65,9 +76,7 @@
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
};
@@ -79,6 +88,14 @@
name = Products;
sourceTree = "<group>";
};
505B0A322CEB3FAB0092FC35 /* Packages */ = {
isa = PBXGroup;
children = (
505B0A312CEB3FAB0092FC35 /* Intelligents */,
);
path = Packages;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
@@ -91,6 +108,7 @@
9D90BE192CCB9876006677DB /* Cookie */ = {
isa = PBXGroup;
children = (
9D1C07262CEC3E8200E1C502 /* IntelligentsPlugin.swift */,
9D90BE172CCB9876006677DB /* CookieManager.swift */,
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */,
@@ -98,25 +116,29 @@
path = Cookie;
sourceTree = "<group>";
};
9D90BE1A2CCB9876006677DB /* plugins */ = {
9D90BE1A2CCB9876006677DB /* Plugins */ = {
isa = PBXGroup;
children = (
9D90BE192CCB9876006677DB /* Cookie */,
);
path = plugins;
path = Plugins;
sourceTree = "<group>";
};
9D90BE242CCB9876006677DB /* App */ = {
isa = PBXGroup;
children = (
9D90BE1A2CCB9876006677DB /* plugins */,
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */,
505B0A322CEB3FAB0092FC35 /* Packages */,
9D90BE1A2CCB9876006677DB /* Plugins */,
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */,
505B0A352CEB48B10092FC35 /* RootViewController.swift */,
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
9D90BE1D2CCB9876006677DB /* Assets.xcassets */,
9D90BE1E2CCB9876006677DB /* capacitor.config.json */,
9D90BE1F2CCB9876006677DB /* config.xml */,
9D90BE202CCB9876006677DB /* Info.plist */,
9D90BE222CCB9876006677DB /* Main.storyboard */,
501FBC552D02F88200507774 /* InfoPlist.xcstrings */,
501FBC572D02F88800507774 /* Localizable.xcstrings */,
9D90BE232CCB9876006677DB /* public */,
);
path = App;
@@ -152,7 +174,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
LastUpgradeCheck = 1610;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
@@ -183,10 +205,12 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
501FBC562D02F88200507774 /* InfoPlist.xcstrings in Resources */,
9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */,
9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */,
9D90BE2B2CCB9876006677DB /* config.xml in Resources */,
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */,
501FBC582D02F88800507774 /* Localizable.xcstrings in Resources */,
9D90BE2E2CCB9876006677DB /* public in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -236,8 +260,10 @@
files = (
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */,
9D1C07272CEC3E9500E1C502 /* IntelligentsPlugin.swift in Sources */,
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */,
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */,
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -260,6 +286,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -270,6 +297,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -277,8 +305,10 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -292,6 +322,7 @@
DEVELOPMENT_TEAM = 73YMMDVT2M;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -313,6 +344,7 @@
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
@@ -321,6 +353,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -331,6 +364,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -338,8 +372,10 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -353,6 +389,7 @@
DEVELOPMENT_TEAM = 73YMMDVT2M;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -367,6 +404,7 @@
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
@@ -382,9 +420,10 @@
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = 73YMMDVT2M;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -412,9 +451,10 @@
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = 73YMMDVT2M;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -454,6 +494,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
505B0A332CEB3FB10092FC35 /* Intelligents */ = {
isa = XCSwiftPackageProductDependency;
productName = Intelligents;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

View File

@@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"state" : {
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version" : "0.5.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 2
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"state" : {
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version" : "0.5.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 2
}

View File

@@ -1,15 +0,0 @@
import UIKit
import Capacitor
class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
}
override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin())
bridge?.registerPluginInstance(HashcashPlugin())
}
}

View File

@@ -0,0 +1,111 @@
import Capacitor
import Intelligents
import UIKit
class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
navigationController?.navigationBar.isHidden = true
extendedLayoutIncludesOpaqueBars = false
edgesForExtendedLayout = []
let intelligentsButton = installIntelligentsButton()
intelligentsButton.delegate = self
dismissIntelligentsButton()
}
override func capacitorDidLoad() {
let plugins: [CAPPlugin] = [
CookiePlugin(),
HashcashPlugin(),
IntelligentsPlugin(representController: self),
]
plugins.forEach { bridge?.registerPluginInstance($0) }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
}
extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate {
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
guard let webView else {
assertionFailure() // ? wdym ?
return
}
button.beginProgress()
let script = "return await window.getCurrentDocContentInMarkdown();"
webView.callAsyncJavaScript(
script,
arguments: [:],
in: nil,
in: .page
) { result in
button.stopProgress()
webView.resignFirstResponder()
if case let .failure(error) = result {
print("[?] \(self) script error: \(error.localizedDescription)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if case let .success(content) = result,
let res = content as? String
{
print("[*] \(self) received document with \(res.count) characters")
DispatchQueue.main.async {
self.openIntelligentsSheet(withContext: res)
}
} else {
DispatchQueue.main.async {
self.openSimpleChat()
}
}
}
}
}
func openIntelligentsSheet(withContext context: String) {
guard let view = webView?.subviews.first else {
assertionFailure()
return
}
assert(view is UIScrollView)
_ = context
let focus = IntelligentsFocusApertureView()
focus.prepareAnimationWith(
capturingTargetContentView: view,
coveringRootViewController: self
)
focus.delegate = self
focus.executeAnimationKickIn()
dismissIntelligentsButton()
}
func openSimpleChat() {
let targetController = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: targetController)
}
func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType) {
switch actionType {
case .translateTo:
fatalError("not implemented")
case .summary:
fatalError("not implemented")
case .chatWithAI:
let controller = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: controller)
case .dismiss:
presentIntelligentsButton()
}
}
}

View File

@@ -1,49 +1,47 @@
import UIKit
import Capacitor
import UIKit
@UIApplicationMain
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var window: UIWindow?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
</dependencies>
<scenes>
<!--FiNE View Controller-->
<!--Root View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="AFFiNEViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<viewController id="BYZ-38-t0r" customClass="RootViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="140" y="-1"/>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
@@ -37,6 +39,8 @@
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>We will take photo with this permission when you asked to do, and we will attach picture to your post or request when possible.</string>
<key>UILaunchScreen</key>
<dict>
<key>UIImageName</key>

View File

@@ -0,0 +1,42 @@
{
"sourceLanguage" : "en",
"strings" : {
"CFBundleDisplayName" : {
"comment" : "Bundle display name",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "AFFiNE"
}
}
}
},
"CFBundleName" : {
"comment" : "Bundle name",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "App"
}
}
}
},
"NSCameraUsageDescription" : {
"comment" : "Privacy - Camera Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "We will take photo with this permission when you asked to do, and we will attach picture to your post or request when possible."
}
}
}
}
},
"version" : "1.0"
}

View File

@@ -0,0 +1,7 @@
{
"sourceLanguage" : "en",
"strings" : {
},
"version" : "1.0"
}

View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Intelligents",
defaultLocalization: "en",
platforms: [
.iOS(.v15),
.macCatalyst(.v15),
],
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
],
dependencies: [
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
],
targets: [
.target(name: "Intelligents", dependencies: [
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
]),
]
)

View File

@@ -0,0 +1,13 @@
//
// Constant.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
enum Constant {
static let affineTabbarHeight: CGFloat = 44
static let affineTintColor: UIColor = .init(red: 30 / 255, green: 150 / 255, blue: 235 / 255, alpha: 1.0)
}

View File

@@ -0,0 +1,19 @@
//
// Ext+String.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
extension String {
func localized() -> String {
let ans = NSLocalizedString(self, bundle: Bundle.module, comment: "")
guard !ans.isEmpty else {
assertionFailure()
return self
}
return ans
}
}

View File

@@ -0,0 +1,14 @@
//
// Ext+UIColor.swift
// Intelligents
//
// Created by on 2024/12/13.
//
import UIKit
extension UIColor {
static var accent: UIColor {
.accent
}
}

View File

@@ -0,0 +1,33 @@
//
// Ext+UIFont.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension UIFont {
static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool = false) -> UIFont {
// Get the style's default pointSize
let traits = UITraitCollection(preferredContentSizeCategory: .large)
let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits)
// Get the font at the default size and preferred weight
var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight)
if italic == true {
font = font.with([.traitItalic])
}
// Setup the font to be auto-scalable
let metrics = UIFontMetrics(forTextStyle: style)
return metrics.scaledFont(for: font)
}
private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont {
guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else {
return self
}
return UIFont(descriptor: descriptor, size: 0)
}
}

View File

@@ -0,0 +1,46 @@
//
// Ext+UIView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension UIView {
var parentViewController: UIViewController? {
var responder: UIResponder? = self
while responder != nil {
if let responder = responder as? UIViewController {
return responder
}
responder = responder?.next
}
return nil
}
func removeEveryAutoResizingMasks() {
var views: [UIView] = [self]
while let view = views.first {
views.removeFirst()
view.translatesAutoresizingMaskIntoConstraints = false
view.subviews.forEach { views.append($0) }
}
}
#if DEBUG
func debugFrame() {
layer.borderWidth = 1
layer.borderColor = [
UIColor.red,
.green,
.blue,
.yellow,
.cyan,
.magenta,
.orange,
].map(\.cgColor).randomElement()
subviews.forEach { $0.debugFrame() }
}
#endif
}

View File

@@ -0,0 +1,38 @@
//
// Ext+UIViewController.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
public extension UIViewController {
func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) {
if let nav = self as? UINavigationController {
nav.pushViewController(targetController, animated: animated)
} else if let nav = navigationController {
nav.pushViewController(targetController, animated: animated)
} else {
present(targetController, animated: animated, completion: nil)
}
}
func dismissInContext() {
if let nav = navigationController {
nav.popViewController(animated: true)
} else {
dismiss(animated: true, completion: nil)
}
}
func hideKeyboardWhenTappedAround() {
let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}

View File

@@ -0,0 +1,14 @@
//
// Ext+print.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
Swift.print(items, separator: separator, terminator: terminator)
#endif
}

View File

@@ -0,0 +1,4 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
enum Intelligents {}

View File

@@ -0,0 +1,84 @@
//
// IntelligentsButton+Control.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
public extension UIViewController {
@discardableResult
func installIntelligentsButton() -> IntelligentsButton {
print("[*] \(#function)")
if let button = findIntelligentsButton() { return button }
let button = IntelligentsButton()
view.addSubview(button)
view.bringSubviewToFront(button)
button.translatesAutoresizingMaskIntoConstraints = false
[
button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20 - Constant.affineTabbarHeight),
button.widthAnchor.constraint(equalToConstant: 50),
button.heightAnchor.constraint(equalToConstant: 50),
].forEach { $0.isActive = true }
button.transform = .init(scaleX: 0, y: 0)
view.layoutIfNeeded()
return button
}
private func findIntelligentsButton() -> IntelligentsButton? {
for subview in view.subviews { // for for depth 1
if let button = subview as? IntelligentsButton {
return button
}
}
return nil
}
func presentIntelligentsButton() {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.alpha = 0
button.isHidden = false
button.setNeedsLayout()
button.stopProgress()
view.layoutIfNeeded()
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
button.alpha = 1
button.transform = .identity
button.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
func dismissIntelligentsButton() {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.stopProgress()
button.setNeedsLayout()
view.layoutIfNeeded()
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
button.alpha = 0
button.transform = .init(scaleX: 0, y: 0)
button.setNeedsLayout()
self.view.layoutIfNeeded()
} completion: { _ in
button.isHidden = true
}
}
}

View File

@@ -0,0 +1,12 @@
//
// IntelligentsButton+Delegate.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import Foundation
public protocol IntelligentsButtonDelegate: AnyObject {
func onIntelligentsButtonTapped(_ button: IntelligentsButton)
}

View File

@@ -0,0 +1,90 @@
//
// IntelligentsButton.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
// floating button to open intelligent panel
public class IntelligentsButton: UIView {
let image = UIImageView()
let background = UIView()
let activityIndicator = UIActivityIndicatorView()
public weak var delegate: (any IntelligentsButtonDelegate)? = nil {
didSet { assert(Thread.isMainThread) }
}
public init() {
super.init(frame: .zero)
background.backgroundColor = .white
addSubview(background)
background.translatesAutoresizingMaskIntoConstraints = false
[
background.leadingAnchor.constraint(equalTo: leadingAnchor),
background.trailingAnchor.constraint(equalTo: trailingAnchor),
background.topAnchor.constraint(equalTo: topAnchor),
background.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
image.image = .init(named: "spark", in: .module, with: .none)
image.contentMode = .scaleAspectFit
image.tintColor = Constant.affineTintColor
addSubview(image)
let imageInsetValue: CGFloat = 12
image.translatesAutoresizingMaskIntoConstraints = false
[
image.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageInsetValue),
image.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -imageInsetValue),
image.topAnchor.constraint(equalTo: topAnchor, constant: imageInsetValue),
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue),
].forEach { $0.isActive = true }
addSubview(activityIndicator)
[
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
].forEach { $0.isActive = true }
clipsToBounds = true
layer.borderWidth = 2
layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(tap)
isUserInteractionEnabled = true
stopProgress()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
deinit {
delegate = nil
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width / 2
}
@objc func tapped() {
delegate?.onIntelligentsButtonTapped(self)
}
public func beginProgress() {
activityIndicator.startAnimating()
activityIndicator.isHidden = false
}
public func stopProgress() {
activityIndicator.stopAnimating()
activityIndicator.isHidden = true
}
}

View File

@@ -0,0 +1,100 @@
//
// ChatTableView+BaseCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
private let initialInsetValue: CGFloat = 24
extension ChatTableView {
class BaseCell: UITableViewCell {
var inset: UIEdgeInsets { // available for overrides
.init(
top: initialInsetValue / 2,
left: initialInsetValue,
bottom: initialInsetValue / 2,
right: initialInsetValue
)
}
let containerView = UIView()
let roundedBackgroundView = UIView()
var viewModel: AnyObject? {
didSet { update(via: viewModel) }
}
enum BackgroundColorType {
case clear
case highlight
case warning
case lightGray
}
var backgroundColorType: BackgroundColorType = .clear {
didSet {
roundedBackgroundView.backgroundColor = backgroundColorType.color
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
roundedBackgroundView.clipsToBounds = true
roundedBackgroundView.layer.cornerRadius = 8
roundedBackgroundView.layer.masksToBounds = true
contentView.addSubview(roundedBackgroundView)
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
[ // inset half of the container view
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left / 2),
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right / 2),
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top / 2),
roundedBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom / 2),
].forEach { $0.isActive = true }
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
[
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right),
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func prepareForReuse() {
super.prepareForReuse()
viewModel = nil
}
func update(via object: AnyObject?) {
_ = object
}
}
}
extension ChatTableView.BaseCell.BackgroundColorType {
var color: UIColor {
switch self {
case .clear:
.clear
case .highlight:
.accent
case .warning:
.systemRed.withAlphaComponent(0.1)
case .lightGray:
.systemGray.withAlphaComponent(0.1)
}
}
}

View File

@@ -0,0 +1,124 @@
//
// ChatTableView+ChatCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import MarkdownUI
import UIKit
extension ChatTableView {
class ChatCell: BaseCell {
let avatarView = CircleImageView()
let titleLabel = UILabel()
let markdownContainer = UIView()
var markdownView: UIView?
var removableConstraints: [NSLayoutConstraint] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let spacingElement: CGFloat = 12
let avatarSize: CGFloat = 24
containerView.addSubview(avatarView)
avatarView.translatesAutoresizingMaskIntoConstraints = false
[
avatarView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
avatarView.topAnchor.constraint(equalTo: containerView.topAnchor),
avatarView.widthAnchor.constraint(equalToConstant: avatarSize),
avatarView.heightAnchor.constraint(equalToConstant: avatarSize),
].forEach { $0.isActive = true }
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .bold)
containerView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
[
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: spacingElement),
titleLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
].forEach { $0.isActive = true }
containerView.addSubview(markdownContainer)
markdownContainer.translatesAutoresizingMaskIntoConstraints = false
[
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: avatarView.bottomAnchor, constant: spacingElement),
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: spacingElement),
markdownContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
markdownContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
markdownContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func update(via object: AnyObject?) {
super.update(via: object)
guard let viewModel = object as? ViewModel else {
return
}
switch viewModel.participant {
case .system:
avatarView.image = UIImage(systemName: "gearshape.fill")
titleLabel.text = "System".localized()
backgroundColorType = .warning
case .assistant:
avatarView.image = UIImage(named: "spark", in: .module, with: .none)
titleLabel.text = "AFFiNE AI".localized()
backgroundColorType = .lightGray
case .user:
avatarView.image = UIImage(systemName: "person.fill")
titleLabel.text = "You".localized()
backgroundColorType = .clear
}
removableConstraints.forEach { $0.isActive = false }
if let markdownView { markdownView.removeFromSuperview() }
markdownContainer.subviews.forEach { $0.removeFromSuperview() }
let hostingView: UIView = UIHostingView(
rootView: Markdown(.init(viewModel.markdownDocument))
)
defer { markdownView = hostingView }
markdownContainer.addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
[
hostingView.topAnchor.constraint(equalTo: markdownContainer.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: markdownContainer.leadingAnchor),
hostingView.trailingAnchor.constraint(lessThanOrEqualTo: markdownContainer.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: markdownContainer.bottomAnchor),
].forEach {
$0.isActive = true
removableConstraints.append($0)
}
}
}
}
extension ChatTableView.ChatCell {
class ViewModel {
let participant: Participant
let markdownDocument: String
init(participant: Participant, markdownDocument: String) {
self.participant = participant
self.markdownDocument = markdownDocument
}
}
}
extension ChatTableView.ChatCell.ViewModel {
enum Participant {
case user
case assistant
case system
}
}

View File

@@ -0,0 +1,57 @@
//
// ChatTableView+Data.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension ChatTableView {
struct DataElement {
enum CellType: String, CaseIterable {
case base
case chat
}
let type: CellType
let object: AnyObject?
init(type: CellType, object: AnyObject?) {
self.type = type
self.object = object
}
}
}
extension ChatTableView.DataElement.CellType {
var cellClassType: ChatTableView.BaseCell.Type {
switch self {
case .base:
ChatTableView.BaseCell.self
case .chat:
ChatTableView.ChatCell.self
}
}
var cellIdentifier: String {
NSStringFromClass(cellClassType)
}
}
extension ChatTableView: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: dataSource[indexPath.row].type.cellIdentifier, for: indexPath) as! BaseCell
let object = dataSource[indexPath.row].object
cell.update(via: object)
return cell
}
}

View File

@@ -0,0 +1,130 @@
//
// ChatTableView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class ChatTableView: UIView {
let tableView = UITableView()
var dataSource: [DataElement] = []
init() {
super.init(frame: .zero)
for eachCase in DataElement.CellType.allCases {
let cellClass = eachCase.cellClassType
tableView.register(cellClass, forCellReuseIdentifier: eachCase.cellIdentifier)
}
tableView.backgroundColor = .clear
tableView.delegate = self
tableView.dataSource = self
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
[
tableView.topAnchor.constraint(equalTo: topAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
let foot = UIView()
foot.translatesAutoresizingMaskIntoConstraints = false
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
tableView.tableFooterView = foot
tableView.separatorStyle = .none
putMockData()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
extension ChatTableView {
func putMockData() {
DispatchQueue.main.async {
let json: [String: Any] = ["query": """
{
currentUser {
email
name
}
}
""", "variables": [:]]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
let url = URL(string: "http://localhost:3010/graphql")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [
"content-type": "application/json"
]
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error)
} else if let data = data {
let str = String(data: data, encoding: .utf8)
self.dataSource = [
.init(type: .chat, object: ChatCell.ViewModel(
participant: .system,
markdownDocument: "Welcome to Intelligents" + str!
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .user,
markdownDocument: "Please summarize this article for me"
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .assistant,
markdownDocument: ###"""
**Activation Code Usage Limits**
A single activation code can be used on multiple devices.
**Note:** A single activation code is intended for use on a reasonable number of devices by one user.
Excessive activation requests may result in the activation code being banned. Any bans are subject to manual review and are operated by staff.
`The limit is up to 5 devices per year or 10 activation requests within the same period.`
"""###
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .user,
markdownDocument: ###"""
**Download Axchange from the App Store**
You can download Axchange from the App Store:
- [https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944](https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944)
The version downloaded this way does not require activation to use.
"""###
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .assistant,
markdownDocument: "GOOD"
)),
]
self.tableView.reloadData()
}
}
task.resume()
}
}
}

View File

@@ -0,0 +1,163 @@
//
// AttachmentBannerView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
private let attachmentSize: CGFloat = 100
private let attachmentSpacing: CGFloat = 16
class AttachmentBannerView: UIScrollView {
var readAttachments: (() -> ([UIImage]))?
var onAttachmentsDelete: ((Int) -> Void)?
var attachments: [UIImage] {
get { readAttachments?() ?? [] }
set { assertionFailure() }
}
override var intrinsicContentSize: CGSize {
if attachments.isEmpty { return .zero }
return .init(
width: (attachmentSize + attachmentSize) * CGFloat(attachments.count)
- attachmentSpacing,
height: attachmentSize
)
}
let stackView = UIStackView()
init() {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
stackView.axis = .horizontal
stackView.spacing = attachmentSpacing
stackView.alignment = .center
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
[
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
rebuildViews()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
var reusableViews = [AttachmentPreviewView]()
func rebuildViews() {
let attachments = attachments
if reusableViews.count > attachments.count {
for index in attachments.count ..< reusableViews.count {
reusableViews[index].removeFromSuperview()
}
reusableViews.removeLast(reusableViews.count - attachments.count)
}
if reusableViews.count < attachments.count {
for _ in reusableViews.count ..< attachments.count {
let view = AttachmentPreviewView()
view.alpha = 0
reusableViews.append(view)
}
}
assert(reusableViews.count == attachments.count)
for (index, attachment) in attachments.enumerated() {
let view = reusableViews[index]
view.imageView.image = attachment
stackView.addArrangedSubview(view)
view.deleteButtonAction = { [weak self] in
self?.onAttachmentsDelete?(index)
}
}
invalidateIntrinsicContentSize()
contentSize = intrinsicContentSize
UIView.performWithoutAnimation {
self.layoutIfNeeded()
}
UIView.animate(withDuration: 0.3) {
for view in self.reusableViews {
view.alpha = 1
}
}
}
}
extension AttachmentBannerView {
class AttachmentPreviewView: UIView {
let imageView = UIImageView()
let deleteButton = UIButton()
var deleteButtonAction: (() -> Void)?
override var intrinsicContentSize: CGSize {
.init(width: attachmentSize, height: attachmentSize)
}
init() {
super.init(frame: .zero)
addSubview(imageView)
addSubview(deleteButton)
layer.cornerRadius = 8
clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
[
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
deleteButton.setImage(.init(named: "close", in: .module, with: nil), for: .normal)
deleteButton.imageView?.contentMode = .scaleAspectFit
deleteButton.tintColor = .white
deleteButton.translatesAutoresizingMaskIntoConstraints = false
[
deleteButton.topAnchor.constraint(equalTo: topAnchor, constant: 4),
deleteButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
deleteButton.widthAnchor.constraint(equalToConstant: 32),
deleteButton.heightAnchor.constraint(equalToConstant: 32),
].forEach { $0.isActive = true }
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
[
widthAnchor.constraint(equalToConstant: attachmentSize),
heightAnchor.constraint(equalToConstant: attachmentSize),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func deleteButtonTapped() {
deleteButtonAction?()
deleteButtonAction = nil
}
}
}

View File

@@ -0,0 +1,65 @@
//
// InputEditView+Camera.swift
// Intelligents
//
// Created by on 2024/12/6.
//
import AVKit
import UIKit
extension InputEditView: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
@objc func takePhoto() {
AVCaptureDevice.requestAccess(for: .video) { _ in
DispatchQueue.main.async {
let ctrl = UIImagePickerController()
ctrl.allowsEditing = false
ctrl.sourceType = .camera
ctrl.mediaTypes = [UTType.movie.identifier, UTType.image.identifier]
ctrl.cameraCaptureMode = .photo
ctrl.delegate = self
self.parentViewController?.present(ctrl, animated: true)
}
}
}
private func processJPEGImageData(_ image: UIImage) throws -> Data? {
guard let data = image.jpegData(compressionQuality: 0.75) else {
throw NSError(domain: "", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Failed to compress image data",
])
}
return data
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
picker.dismiss(animated: true) {
var itemUrl: URL?
if itemUrl == nil,
let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage
{
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("Camera")
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let tempFile = tempDir
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("jpeg")
try? self.processJPEGImageData(image)?.write(to: tempFile)
itemUrl = tempFile
}
if itemUrl == nil,
let url = info[.mediaURL] as? URL
{
itemUrl = url
}
guard let url = itemUrl, FileManager.default.fileExists(atPath: url.path) else {
return
}
guard let image = UIImage(contentsOfFile: url.path) else { return }
try? FileManager.default.removeItem(at: url)
self.viewModel.attachments.append(image)
}
}
}

View File

@@ -0,0 +1,38 @@
//
// InputEditView+Photo.swift
// Intelligents
//
// Created by on 2024/12/6.
//
import PhotosUI
import UIKit
extension InputEditView: PHPickerViewControllerDelegate {
@objc func selectPhoto() {
var config = PHPickerConfiguration(photoLibrary: .shared())
config.filter = .images
config.selectionLimit = 9
let picker = PHPickerViewController(configuration: config)
picker.modalPresentationStyle = .formSheet
picker.delegate = self
parentViewController?.present(picker, animated: true, completion: nil)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
loadPNG(from: results)
}
private func loadPNG(from results: [PHPickerResult]) {
for result in results {
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.viewModel.attachments.append(image)
}
}
}
}
}
}

View File

@@ -0,0 +1,48 @@
//
// InputEditView+ViewModel.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Combine
import UIKit
extension InputEditView {
class ViewModel: ObservableObject {
var cancellables: Set<AnyCancellable> = []
@Published var text: String = ""
@Published var attachments: [UIImage] = []
init() {}
deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
func reset() {
text = ""
attachments = []
}
func duplicate() -> ViewModel {
let ans = ViewModel()
ans.text = text
ans.attachments = attachments
return ans
}
}
}
extension InputEditView.ViewModel: Hashable, Equatable {
func hash(into hasher: inout Hasher) {
hasher.combine(text)
hasher.combine(attachments)
}
static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool {
lhs.hashValue == rhs.hashValue
}
}

View File

@@ -0,0 +1,132 @@
//
// InputEditView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Combine
import UIKit
class InputEditView: UIView, UITextViewDelegate {
let mainStack = UIStackView()
let attachmentsEditor = AttachmentBannerView()
let textEditor = PlainTextEditView()
let placeholderLabel = UILabel()
let controlBanner = TextEditControlBanner()
let viewModel = ViewModel()
var placeholderText: String = "" {
didSet {
placeholderLabel.text = placeholderText
}
}
init() {
super.init(frame: .zero)
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.axis = .vertical
mainStack.spacing = 16
mainStack.alignment = .fill
mainStack.distribution = .equalSpacing
[
mainStack.topAnchor.constraint(equalTo: topAnchor),
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
textEditor.delegate = self
textEditor.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true
[
attachmentsEditor, textEditor, controlBanner,
].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
mainStack.addArrangedSubview($0)
[
$0.leadingAnchor.constraint(equalTo: mainStack.leadingAnchor),
$0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor),
].forEach { $0.isActive = true }
}
attachmentsEditor.readAttachments = { [weak self] in
self?.viewModel.attachments ?? []
}
attachmentsEditor.onAttachmentsDelete = { [weak self] index in
self?.viewModel.attachments.remove(at: index)
}
controlBanner.cameraButton.addTarget(
self,
action: #selector(takePhoto),
for: .touchUpInside
)
controlBanner.photoButton.addTarget(
self,
action: #selector(selectPhoto),
for: .touchUpInside
)
textEditor.returnKeyType = .send
textEditor.addSubview(placeholderLabel)
placeholderLabel.textColor = .label.withAlphaComponent(0.25)
placeholderLabel.font = textEditor.font
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
[
placeholderLabel.leadingAnchor.constraint(equalTo: textEditor.leadingAnchor, constant: 2),
placeholderLabel.trailingAnchor.constraint(equalTo: textEditor.trailingAnchor, constant: -2),
placeholderLabel.topAnchor.constraint(equalTo: textEditor.topAnchor, constant: 0),
].forEach { $0.isActive = true }
viewModel.objectWillChange
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateValues()
}
.store(in: &viewModel.cancellables)
updateValues()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
func textViewDidChange(_ textView: UITextView) {
viewModel.text = textView.text
}
func textViewDidBeginEditing(_: UITextView) {
updatePlaceholderVisibility()
}
func textViewDidEndEditing(_: UITextView) {
updatePlaceholderVisibility()
}
func updatePlaceholderVisibility() {
let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder
UIView.animate(withDuration: 0.25) {
self.placeholderLabel.alpha = visible ? 1 : 0
}
}
func updateValues() {
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) { [self] in
if textEditor.text != viewModel.text {
textEditor.text = viewModel.text
}
attachmentsEditor.rebuildViews()
parentViewController?.view.layoutIfNeeded()
}
}
}

View File

@@ -0,0 +1,37 @@
//
// PlainTextEditView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class PlainTextEditView: UITextView, UITextViewDelegate {
init() {
super.init(frame: .zero, textContainer: nil)
delegate = self
tintColor = Constant.affineTintColor
linkTextAttributes = [:]
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
textContainer.lineFragmentPadding = .zero
textAlignment = .natural
backgroundColor = .clear
textContainerInset = .zero
textContainer.lineBreakMode = .byTruncatingTail
isScrollEnabled = false
clipsToBounds = false
isEditable = true
isSelectable = true
isScrollEnabled = false
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}

View File

@@ -0,0 +1,62 @@
//
// TextEditControlBanner.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class TextEditControlBanner: UIStackView {
static let height: CGFloat = 32
let cameraButton = UIButton()
let photoButton = UIButton()
let spacer = UIView()
let sendButton = UIButton()
init() {
super.init(frame: .zero)
axis = .horizontal
spacing = 16
alignment = .center
distribution = .fill
[
heightAnchor.constraint(equalToConstant: Self.height),
].forEach { $0.isActive = true }
[
cameraButton, photoButton,
sendButton,
].forEach {
$0.widthAnchor.constraint(equalToConstant: Self.height).isActive = true
$0.heightAnchor.constraint(equalToConstant: Self.height).isActive = true
}
[
cameraButton, photoButton,
spacer,
sendButton,
].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addArrangedSubview($0)
}
cameraButton.setImage(.init(systemName: "camera"), for: .normal)
cameraButton.tintColor = .label
photoButton.setImage(.init(systemName: "photo"), for: .normal)
photoButton.tintColor = .label
sendButton.setImage(.init(systemName: "paperplane.fill"), for: .normal)
sendButton.tintColor = .label
}
@available(*, unavailable)
required init(coder _: NSCoder) {
fatalError()
}
}

View File

@@ -0,0 +1,108 @@
//
// IntelligentsChatController+Header.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension IntelligentsChatController {
class Header: UIView {
static let height: CGFloat = 44
let contentView = UIView()
let titleLabel = UILabel()
let dropMenu = UIButton()
let backButton = UIButton()
let rightBarItemsStack = UIStackView()
let moreMenu = UIButton()
init() {
super.init(frame: .zero)
setupLayout()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func navigateActionBack() {
parentViewController?.dismissInContext()
}
}
}
private extension IntelligentsChatController.Header {
func setupLayout() {
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
[
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.heightAnchor.constraint(equalToConstant: Self.height),
].forEach { $0.isActive = true }
titleLabel.textColor = .label
titleLabel.font = .systemFont(
ofSize: UIFont.labelFontSize,
weight: .semibold
)
backButton.setImage(
UIImage(systemName: "chevron.left"),
for: .normal
)
backButton.tintColor = Constant.affineTintColor
backButton.addTarget(self, action: #selector(navigateActionBack), for: .touchUpInside)
dropMenu.setImage(
.init(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate),
for: .normal
)
dropMenu.tintColor = .gray.withAlphaComponent(0.5)
contentView.addSubview(titleLabel)
contentView.addSubview(backButton)
contentView.addSubview(dropMenu)
contentView.addSubview(rightBarItemsStack)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.translatesAutoresizingMaskIntoConstraints = false
dropMenu.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.axis = .horizontal
rightBarItemsStack.spacing = 10
rightBarItemsStack.alignment = .center
rightBarItemsStack.distribution = .equalSpacing
[
backButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
backButton.widthAnchor.constraint(equalToConstant: 44),
backButton.heightAnchor.constraint(equalToConstant: 44),
rightBarItemsStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
rightBarItemsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
rightBarItemsStack.heightAnchor.constraint(equalToConstant: 44),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: backButton.trailingAnchor, constant: 10),
dropMenu.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
dropMenu.widthAnchor.constraint(equalToConstant: 44),
dropMenu.heightAnchor.constraint(equalToConstant: 44),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: dropMenu.leadingAnchor, constant: -10),
].forEach { $0.isActive = true }
rightBarItemsStack.addArrangedSubview(moreMenu)
moreMenu.setImage(
.init(systemName: "ellipsis.circle"),
for: .normal
)
moreMenu.tintColor = Constant.affineTintColor
}
}

View File

@@ -0,0 +1,61 @@
//
// IntelligentsChatController+InputBox.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension IntelligentsChatController {
class InputBox: UIView {
let backgroundView = UIView()
let editor = InputEditView()
init() {
super.init(frame: .zero)
setupLayout()
editor.textEditor.font = UIFont.systemFont(ofSize: UIFont.labelFontSize)
editor.placeholderText = "Summarize this article for me...".localized()
backgroundView.backgroundColor = .systemBackground
backgroundView.layer.cornerRadius = 16
backgroundView.layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor
backgroundView.layer.shadowOffset = .init(width: 0, height: 0)
backgroundView.layer.shadowRadius = 8
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}
private extension IntelligentsChatController.InputBox {
func setupLayout() {
addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(editor)
editor.translatesAutoresizingMaskIntoConstraints = false
let inset: CGFloat = 16
[
editor.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset),
editor.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset),
editor.topAnchor.constraint(equalTo: topAnchor, constant: inset),
editor.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset),
].forEach { $0.isActive = true }
[
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0),
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 128),
].forEach { $0.isActive = true }
}
}

View File

@@ -0,0 +1,43 @@
//
// IntelligentsChatController+Keyboard.swift
// Intelligents
//
// Created by on 2024/12/6.
//
import UIKit
extension IntelligentsChatController {
@objc func keyboardWillAppear(_ notification: Notification) {
let info = notification.userInfo ?? [:]
let keyboardHeight = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?
.cgRectValue
.height ?? 0
inputBoxKeyboardAdapterHeightConstraint.constant = keyboardHeight
view.setNeedsUpdateConstraints()
animateWithKeyboard(userInfo: info)
}
@objc func keyboardWillDisappear(_ notification: Notification) {
let info = notification.userInfo ?? [:]
inputBoxKeyboardAdapterHeightConstraint.constant = 0
view.setNeedsUpdateConstraints()
animateWithKeyboard(userInfo: info)
}
private func animateWithKeyboard(userInfo info: [AnyHashable: Any]) {
let keyboardAnimationDuration = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?
.doubleValue ?? 0
let keyboardAnimationCurve = (info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?
.uintValue ?? 0
UIView.animate(
withDuration: keyboardAnimationDuration,
delay: 0,
options: UIView.AnimationOptions(rawValue: keyboardAnimationCurve),
animations: {
self.view.layoutIfNeeded()
},
completion: nil
)
}
}

View File

@@ -0,0 +1,151 @@
//
// IntelligentsChatController.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
public class IntelligentsChatController: UIViewController {
let header = Header()
let inputBoxKeyboardAdapter = UIView()
let inputBox = InputBox()
let progressView = UIActivityIndicatorView()
let tableView = ChatTableView()
var inputBoxKeyboardAdapterHeightConstraint = NSLayoutConstraint()
override public var title: String? {
set {
super.title = newValue
header.titleLabel.text = newValue
}
get {
super.title
}
}
public init() {
super.init(nibName: nil, bundle: nil)
title = "Chat with AI".localized()
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillDisappear),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillAppear),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
override public func viewDidLoad() {
super.viewDidLoad()
assert(navigationController != nil)
view.backgroundColor = .secondarySystemBackground
hideKeyboardWhenTappedAround()
setupLayout()
}
func setupLayout() {
view.addSubview(header)
header.translatesAutoresizingMaskIntoConstraints = false
[
header.topAnchor.constraint(equalTo: view.topAnchor),
header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor),
header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44),
].forEach { $0.isActive = true }
view.addSubview(inputBoxKeyboardAdapter)
inputBoxKeyboardAdapter.translatesAutoresizingMaskIntoConstraints = false
[
inputBoxKeyboardAdapter.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBoxKeyboardAdapter.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBoxKeyboardAdapter.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
].forEach { $0.isActive = true }
inputBoxKeyboardAdapterHeightConstraint = inputBoxKeyboardAdapter.heightAnchor.constraint(equalToConstant: 0)
inputBoxKeyboardAdapterHeightConstraint.isActive = true
inputBoxKeyboardAdapter.backgroundColor = inputBox.backgroundView.backgroundColor
view.addSubview(inputBox)
inputBox.translatesAutoresizingMaskIntoConstraints = false
[
inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBox.bottomAnchor.constraint(equalTo: inputBoxKeyboardAdapter.topAnchor),
].forEach { $0.isActive = true }
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
[
tableView.topAnchor.constraint(equalTo: header.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: inputBox.topAnchor),
].forEach { $0.isActive = true }
view.addSubview(progressView)
progressView.hidesWhenStopped = true
progressView.stopAnimating()
progressView.translatesAutoresizingMaskIntoConstraints = false
[
progressView.centerXAnchor.constraint(equalTo: inputBox.centerXAnchor),
progressView.centerYAnchor.constraint(equalTo: inputBox.centerYAnchor),
].forEach { $0.isActive = true }
progressView.style = .large
inputBox.editor.controlBanner.sendButton.addTarget(
self,
action: #selector(send),
for: .touchUpInside
)
}
@objc func send() {
assert(Thread.isMainThread)
inputBox.isUserInteractionEnabled = false
progressView.startAnimating()
progressView.isHidden = false
progressView.alpha = 0
UIView.animate(withDuration: 0.3) {
self.inputBox.editor.alpha = 0
self.progressView.alpha = 1
} completion: { _ in
let viewModel = self.inputBox.editor.viewModel.duplicate()
self.inputBox.editor.viewModel.reset()
DispatchQueue.global().async {
self.sendSyncEx(viewModel: viewModel)
DispatchQueue.main.async {
UIView.animate(withDuration: 0.3) {
self.inputBox.editor.alpha = 1
self.progressView.alpha = 0
} completion: { _ in
self.inputBox.isUserInteractionEnabled = true
self.progressView.stopAnimating()
}
}
}
}
}
private func sendSyncEx(viewModel: InputEditView.ViewModel) {
let text = viewModel.text
let images = viewModel.attachments
}
}

View File

@@ -0,0 +1,76 @@
//
// IntelligentsFocusApertureView+ActionButton.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView.ControlButtonsPanel {
class DarkActionButton: UIView {
var iconSystemName: String {
set { iconView.image = UIImage(systemName: newValue) }
get { fatalError() }
}
var title: String {
set { titleLabel.text = newValue }
get { titleLabel.text ?? "" }
}
let titleLabel = UILabel()
let iconView = UIImageView()
var action: (() -> Void)? = nil
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white.withAlphaComponent(0.25)
layer.cornerRadius = 12
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
titleLabel.textAlignment = .center
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .semibold)
titleLabel.textColor = .white
addSubview(titleLabel)
iconView.contentMode = .scaleAspectFit
iconView.tintColor = .white
addSubview(iconView)
[
layoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
layoutGuide.centerYAnchor.constraint(equalTo: centerYAnchor),
iconView.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
iconView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
iconView.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
titleLabel.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
].forEach { $0.isActive = true }
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(onTapped)
))
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func onTapped() {
action?()
}
}
}

View File

@@ -0,0 +1,22 @@
//
// IntelligentsFocusApertureView+Capture.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView {
func captureImageBuffer(_ targetContentView: UIView) {
let imageSize = targetContentView.frame.size
let renderer = UIGraphicsImageRenderer(size: imageSize)
let image = renderer.image { _ in
targetContentView.drawHierarchy(
in: targetContentView.bounds,
afterScreenUpdates: false
)
}
capturedImage = image
}
}

View File

@@ -0,0 +1,19 @@
//
// IntelligentsFocusApertureView+Delegate.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import Foundation
public enum IntelligentsFocusApertureViewActionType: String {
case translateTo
case summary
case chatWithAI
case dismiss
}
public protocol IntelligentsFocusApertureViewDelegate: AnyObject {
func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType)
}

View File

@@ -0,0 +1,88 @@
//
// IntelligentsFocusApertureView+Layout.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView {
func prepareFrameLayout() {
guard let viewController = targetViewController,
let view = viewController.view
else {
assertionFailure()
return
}
let safeLayout = viewController.view.safeAreaLayoutGuide
frameConstraints = [
// use safe area to layout content views
leadingAnchor.constraint(equalTo: safeLayout.leadingAnchor),
trailingAnchor.constraint(equalTo: safeLayout.trailingAnchor),
topAnchor.constraint(equalTo: safeLayout.topAnchor),
bottomAnchor.constraint(equalTo: safeLayout.bottomAnchor),
// cover all safe area so use constraints over view
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
}
func prepareContentLayouts() {
guard let targetView else {
assertionFailure()
return
}
contentBeginConstraints = [
snapshotView.leftAnchor.constraint(equalTo: targetView.leftAnchor),
snapshotView.rightAnchor.constraint(equalTo: targetView.rightAnchor),
snapshotView.topAnchor.constraint(equalTo: targetView.topAnchor),
snapshotView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor),
controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor),
controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor),
controlButtonsPanel.topAnchor.constraint(equalTo: bottomAnchor),
]
let sharedInset: CGFloat = 32
contentFinalConstraints = [
snapshotView.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
snapshotView.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
snapshotView.topAnchor.constraint(equalTo: topAnchor),
snapshotView.bottomAnchor.constraint(equalTo: controlButtonsPanel.topAnchor, constant: -sharedInset / 2),
controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
controlButtonsPanel.bottomAnchor.constraint(equalTo: bottomAnchor),
]
}
enum LayoutType {
case begin
case complete
}
func activateLayoutForAnimation(_ type: LayoutType) {
NSLayoutConstraint.activate(frameConstraints)
switch type {
case .begin:
NSLayoutConstraint.deactivate(contentFinalConstraints)
NSLayoutConstraint.activate(contentBeginConstraints)
snapshotView.layer.cornerRadius = 0
case .complete:
NSLayoutConstraint.deactivate(contentBeginConstraints)
NSLayoutConstraint.activate(contentFinalConstraints)
snapshotView.layer.cornerRadius = 32
}
let effectiveView = superview ?? self
effectiveView.setNeedsUpdateConstraints()
effectiveView.setNeedsLayout()
updateConstraints()
layoutIfNeeded()
}
}

View File

@@ -0,0 +1,115 @@
//
// IntelligentsFocusApertureView+Panel.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView {
class ControlButtonsPanel: UIView {
let headerLabel = UILabel()
let headerIcon = UIImageView()
let translateButton = DarkActionButton()
let summaryButton = DarkActionButton()
let chatWithAIButton = DarkActionButton()
init() {
super.init(frame: .zero)
defer { removeEveryAutoResizingMasks() }
let contentSpacing: CGFloat = 16
let buttonGroupHeight: CGFloat = 55
let headerGroup = UIView()
addSubview(headerGroup)
[
headerGroup.topAnchor.constraint(equalTo: topAnchor),
headerGroup.leadingAnchor.constraint(equalTo: leadingAnchor),
headerGroup.trailingAnchor.constraint(equalTo: trailingAnchor),
].forEach { $0.isActive = true }
headerLabel.text = NSLocalizedString("AFFiNE AI", comment: "") // TODO: FREE TRAIL???
// title 3 with bold
headerLabel.font = .preferredFont(for: .title3, weight: .bold)
headerLabel.textColor = .white
headerLabel.textAlignment = .left
headerIcon.image = .init(named: "spark", in: .module, with: nil)
headerIcon.contentMode = .scaleAspectFit
headerIcon.tintColor = Constant.affineTintColor
headerGroup.addSubview(headerLabel)
headerGroup.addSubview(headerIcon)
[
headerLabel.topAnchor.constraint(equalTo: headerGroup.topAnchor),
headerLabel.leadingAnchor.constraint(equalTo: headerGroup.leadingAnchor),
headerLabel.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor),
headerIcon.topAnchor.constraint(equalTo: headerGroup.topAnchor),
headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor),
headerIcon.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor),
headerIcon.widthAnchor.constraint(equalToConstant: 32),
headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor),
headerIcon.leadingAnchor.constraint(equalTo: headerLabel.trailingAnchor, constant: contentSpacing),
].forEach { $0.isActive = true }
let firstButtonSectionGroup = UIView()
addSubview(firstButtonSectionGroup)
[
firstButtonSectionGroup.topAnchor.constraint(equalTo: headerGroup.bottomAnchor, constant: contentSpacing),
firstButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor),
firstButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor),
firstButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight),
].forEach { $0.isActive = true }
translateButton.title = NSLocalizedString("Translate", comment: "")
translateButton.iconSystemName = "textformat"
summaryButton.title = NSLocalizedString("Summary", comment: "")
summaryButton.iconSystemName = "doc.text"
firstButtonSectionGroup.addSubview(translateButton)
firstButtonSectionGroup.addSubview(summaryButton)
[
translateButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor),
translateButton.leadingAnchor.constraint(equalTo: firstButtonSectionGroup.leadingAnchor),
translateButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor),
summaryButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor),
summaryButton.trailingAnchor.constraint(equalTo: firstButtonSectionGroup.trailingAnchor),
summaryButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor),
translateButton.widthAnchor.constraint(equalTo: summaryButton.widthAnchor),
translateButton.trailingAnchor.constraint(equalTo: summaryButton.leadingAnchor, constant: -contentSpacing),
].forEach { $0.isActive = true }
let secondButtonSectionGroup = UIView()
addSubview(secondButtonSectionGroup)
[
secondButtonSectionGroup.topAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor, constant: contentSpacing),
secondButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor),
secondButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor),
secondButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight),
].forEach { $0.isActive = true }
secondButtonSectionGroup.addSubview(chatWithAIButton)
chatWithAIButton.title = NSLocalizedString("Chat with AI", comment: "")
chatWithAIButton.iconSystemName = "paperplane"
[
chatWithAIButton.topAnchor.constraint(equalTo: secondButtonSectionGroup.topAnchor),
chatWithAIButton.leadingAnchor.constraint(equalTo: secondButtonSectionGroup.leadingAnchor),
chatWithAIButton.bottomAnchor.constraint(equalTo: secondButtonSectionGroup.bottomAnchor),
chatWithAIButton.trailingAnchor.constraint(equalTo: secondButtonSectionGroup.trailingAnchor),
].forEach { $0.isActive = true }
[
secondButtonSectionGroup.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}

View File

@@ -0,0 +1,128 @@
//
// IntelligentsFocusApertureView.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
public class IntelligentsFocusApertureView: UIView {
let backgroundView = UIView()
let snapshotView = UIImageView()
let controlButtonsPanel = ControlButtonsPanel()
public var animationDuration: TimeInterval = 0.75
public internal(set) weak var targetView: UIView?
public internal(set) weak var targetViewController: UIViewController?
public internal(set) weak var capturedImage: UIImage? {
get { snapshotView.image }
set { snapshotView.image = newValue }
}
var frameConstraints: [NSLayoutConstraint] = []
var contentBeginConstraints: [NSLayoutConstraint] = []
var contentFinalConstraints: [NSLayoutConstraint] = []
public weak var delegate: (any IntelligentsFocusApertureViewDelegate)?
public init() {
super.init(frame: .zero)
backgroundView.backgroundColor = .black
backgroundView.isUserInteractionEnabled = true
backgroundView.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(dismissFocus)
))
snapshotView.setContentHuggingPriority(.defaultLow, for: .vertical)
snapshotView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
snapshotView.layer.contentsGravity = .top
snapshotView.layer.masksToBounds = true
snapshotView.contentMode = .scaleAspectFill
snapshotView.isUserInteractionEnabled = true
snapshotView.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(dismissFocus)
))
addSubview(backgroundView)
addSubview(controlButtonsPanel)
addSubview(snapshotView)
bringSubviewToFront(snapshotView)
controlButtonsPanel.translateButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .translateTo)
}
controlButtonsPanel.summaryButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .summary)
}
controlButtonsPanel.chatWithAIButton.action = { [weak self] in
self?.delegate?.focusApertureRequestAction(actionType: .chatWithAI)
}
removeEveryAutoResizingMasks()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
public func prepareAnimationWith(
capturingTargetContentView targetContentView: UIView,
coveringRootViewController viewController: UIViewController
) {
captureImageBuffer(targetContentView)
targetView = targetContentView
targetViewController = viewController
viewController.view.addSubview(self)
prepareFrameLayout()
prepareContentLayouts()
activateLayoutForAnimation(.begin)
}
public func executeAnimationKickIn(_ completion: @escaping () -> Void = {}) {
activateLayoutForAnimation(.begin)
isUserInteractionEnabled = false
UIView.animate(
withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
self.activateLayoutForAnimation(.complete)
} completion: { _ in
self.isUserInteractionEnabled = true
completion()
}
}
public func executeAnimationDismiss(_ completion: @escaping () -> Void = {}) {
activateLayoutForAnimation(.complete)
isUserInteractionEnabled = false
UIView.animate(
withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
self.activateLayoutForAnimation(.begin)
} completion: { _ in
self.isUserInteractionEnabled = true
completion()
}
}
@objc func dismissFocus() {
isUserInteractionEnabled = false
executeAnimationDismiss {
self.removeFromSuperview()
self.delegate?.focusApertureRequestAction(actionType: .dismiss)
}
}
}

View File

@@ -0,0 +1,27 @@
//
// Chat.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
struct Chat: Codable {
enum ParticipantType: String, Codable, Equatable {
case user
case bot
}
var participant: ParticipantType
typealias MarkdownDocument = String
var content: MarkdownDocument
var date: Date
init(participant: ParticipantType, content: MarkdownDocument, date: Date = .init()) {
self.participant = participant
self.content = content
self.date = date
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "228",
"green": "148",
"red": "72"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "close.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,5 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" fill="#2A2A2A"/>
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" stroke="#E6E6E6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.1572 5.73483C6.30364 5.58839 6.54108 5.58839 6.68753 5.73483L9.42236 8.46967L12.1572 5.73484C12.3036 5.58839 12.5411 5.58839 12.6875 5.73484C12.834 5.88128 12.834 6.11872 12.6875 6.26517L9.95269 9L12.6875 11.7348C12.834 11.8813 12.834 12.1187 12.6875 12.2652C12.5411 12.4116 12.3036 12.4116 12.1572 12.2652L9.42236 9.53033L6.68753 12.2652C6.54108 12.4116 6.30364 12.4116 6.1572 12.2652C6.01075 12.1187 6.01075 11.8813 6.1572 11.7348L8.89203 9L6.1572 6.26516C6.01075 6.11872 6.01075 5.88128 6.1572 5.73483Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "spark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.1738 5.49104C12.1329 5.13024 11.8278 4.85751 11.4647 4.85714C11.1016 4.85677 10.796 5.12887 10.7544 5.48959C10.4839 7.83515 9.78261 9.48448 8.65126 10.6158C7.51992 11.7472 5.87058 12.4485 3.52502 12.719C3.16431 12.7606 2.89221 13.0662 2.89258 13.4293C2.89295 13.7924 3.16568 14.0975 3.52647 14.1383C5.83327 14.3996 7.51766 15.1006 8.67586 16.2379C9.82971 17.3709 10.5456 19.0198 10.7525 21.3489C10.7853 21.7178 11.0945 22.0004 11.4648 22C11.8351 21.9996 12.1437 21.7162 12.1756 21.3473C12.3739 19.0565 13.0892 17.3729 14.2487 16.2133C15.4083 15.0537 17.092 14.3385 19.3827 14.1402C19.7517 14.1083 20.035 13.7997 20.0354 13.4294C20.0359 13.0591 19.7532 12.7499 19.3843 12.7171C17.0553 12.5102 15.4063 11.7943 14.2733 10.6404C13.136 9.48222 12.435 7.79783 12.1738 5.49104Z"
fill="#1E96EB" />
<path
d="M19.8353 2.24651C19.8194 2.1062 19.7007 2.00014 19.5595 2C19.4183 1.99986 19.2995 2.10567 19.2833 2.24595C19.1781 3.15811 18.9054 3.79952 18.4654 4.23949C18.0254 4.67946 17.384 4.95218 16.4719 5.05739C16.3316 5.07356 16.2258 5.19241 16.2259 5.33362C16.2261 5.47482 16.3321 5.59345 16.4724 5.60935C17.3695 5.71096 18.0246 5.98357 18.475 6.42584C18.9237 6.86644 19.2021 7.50771 19.2826 8.41347C19.2953 8.55691 19.4156 8.66683 19.5596 8.66667C19.7036 8.6665 19.8236 8.55632 19.836 8.41284C19.9131 7.52199 20.1913 6.86723 20.6422 6.41629C21.0931 5.96534 21.7479 5.68719 22.6388 5.61008C22.7822 5.59766 22.8924 5.47765 22.8926 5.33365C22.8927 5.18964 22.7828 5.06939 22.6394 5.05664C21.7336 4.97619 21.0924 4.69777 20.6517 4.24905C20.2095 3.79864 19.9369 3.1436 19.8353 2.24651Z"
fill="#1E96EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,31 @@
/*
Localizable.strings
Intelligents
Created by 秋星桥 on 2024/11/18.
*/
/* No comment provided by engineer. */
"Chat with AI" = "Chat with AI";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE AI";
/* No comment provided by engineer. */
"Translate" = "Translate";
/* No comment provided by engineer. */
"Summary" = "Summary";
/* No comment provided by engineer. */
"Summarize this article for me..." = "Summarize this article for me...";
/* No comment provided by engineer. */
"System" = "System";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE AI";
/* No comment provided by engineer. */
"You" = "You";

View File

@@ -0,0 +1,31 @@
/*
Localizable.strings
Intelligents
Created by 秋星桥 on 2024/11/18.
*/
/* No comment provided by engineer. */
"Chat with AI" = "与 AI 聊天";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE 人工智能";
/* No comment provided by engineer. */
"Translate" = "翻译";
/* No comment provided by engineer. */
"Summary" = "总结";
/* No comment provided by engineer. */
"Summarize this article for me..." = "请为我总结这份文档...";
/* No comment provided by engineer. */
"System" = "系统";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE AI";
/* No comment provided by engineer. */
"You" = "你";

View File

@@ -0,0 +1,29 @@
//
// CircleImageView.swift
// Intelligents
//
// Created by on 2024/12/13.
//
import UIKit
class CircleImageView: UIImageView {
init() {
super.init(frame: .zero)
contentMode = .scaleAspectFill
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
layer.cornerRadius = (bounds.width + bounds.height) / 2 / 2
layer.masksToBounds = true
}
}

View File

@@ -0,0 +1,47 @@
//
// UIHostingView.swift
// Intelligents
//
// Created by on 2024/12/13.
//
import SwiftUI
import UIKit
class UIHostingView<Content: View>: UIView {
private let hostingViewController: UIHostingController<Content>
var rootView: Content {
get { hostingViewController.rootView }
set { hostingViewController.rootView = newValue }
}
init(rootView: Content) {
hostingViewController = UIHostingController(rootView: rootView)
super.init(frame: .zero)
hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false
addSubview(hostingViewController.view)
if let view = hostingViewController.view {
view.backgroundColor = .clear
view.isOpaque = false
addSubview(view)
let constraints = [
view.topAnchor.constraint(equalTo: topAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.leftAnchor.constraint(equalTo: leftAnchor),
view.rightAnchor.constraint(equalTo: rightAnchor),
]
NSLayoutConstraint.activate(constraints)
}
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
hostingViewController.sizeThatFits(in: size)
}
}

View File

@@ -7,24 +7,24 @@ public class CookieManager: NSObject {
let jar = HTTPCookieStorage.shared
guard let url = getServerUrl(urlString) else { return [:] }
if let cookies = jar.cookies(for: url) {
for cookie in cookies {
cookiesMap[cookie.name] = cookie.value
}
for cookie in cookies {
cookiesMap[cookie.name] = cookie.value
}
}
return cookiesMap
}
private func isUrlSanitized(_ urlString: String) -> Bool {
return urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
}
public func getServerUrl(_ urlString: String) -> URL? {
let validUrlString = (isUrlSanitized(urlString)) ? urlString : "http://\(urlString)"
let validUrlString = isUrlSanitized(urlString) ? urlString : "http://\(urlString)"
guard let url = URL(string: validUrlString) else {
return nil
}
guard let url = URL(string: validUrlString) else {
return nil
}
return url
return url
}
}

View File

@@ -1,21 +1,21 @@
import Foundation
import Capacitor
import Foundation
@objc(CookiePlugin)
public class CookiePlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CookiePlugin"
public let jsName = "Cookie"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise)
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
]
let cookieManager = CookieManager()
@objc public func getCookies(_ call: CAPPluginCall) {
guard let url = call.getString("url") else {
return call.resolve([:])
}
call.resolve(cookieManager.getCookies(url))
}
}

View File

@@ -0,0 +1,107 @@
import Capacitor
import CryptoSwift
@objc(HashcashPlugin)
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "HashcashPlugin"
public let jsName = "Hashcash"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise),
]
@objc func hash(_ call: CAPPluginCall) {
DispatchQueue.global(qos: .default).async {
let challenge = call.getString("challenge") ?? ""
let bits = call.getInt("bits") ?? 20
call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()])
}
}
}
let SALT_LENGTH = 16
struct Stamp {
let version: String
let claim: UInt32
let ts: String
let resource: String
let ext: String
let rand: String
let counter: String
func checkExpiration() -> Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
guard let date = dateFormatter.date(from: ts) else { return false }
return Date().addingTimeInterval(5 * 60) <= date
}
func check(bits: UInt32, resource: String) -> Bool {
if version == "1", bits <= claim, checkExpiration(), self.resource == resource {
let hexDigits = Int(floor(Float(claim) / 4.0))
// Check challenge
let formatted = format()
let result = formatted.data(using: .utf8)!.sha3(.sha256).compactMap { String(format: "%02x", $0) }.joined()
return result.prefix(hexDigits) == String(repeating: "0", count: hexDigits)
} else {
return false
}
}
func format() -> String {
"\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
}
static func mint(resource: String, bits: UInt32? = nil) -> Stamp {
let version = "1"
let now = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
let ts = dateFormatter.string(from: now)
let bits = bits ?? 20
let rand = String((0 ..< SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
var counter = 0
var counterHex = ""
var hasher = SHA3(variant: .sha256)
while true {
let toHash = "\(challenge):\(String(format: "%x", counter))"
let hashed = try! hasher.finish(withBytes: toHash.data(using: .utf8)!.bytes)
let result = hashed.compactMap { String(format: "%02x", $0) }.joined()
if result.prefix(hexDigits) == zeros {
counterHex = String(format: "%x", counter)
break
}
counter += 1
}
return Stamp(version: version, claim: bits, ts: ts, resource: resource, ext: "", rand: rand, counter: counterHex)
}
}
extension Stamp {
init?(from string: String) throws {
let parts = string.split(separator: ":")
guard parts.count == 7 else {
throw NSError(domain: "StampError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp, expected 7 parts, got \(parts.count)"])
}
guard let claim = UInt32(parts[1]) else {
throw NSError(domain: "StampError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp"])
}
version = String(parts[0])
self.claim = claim
ts = String(parts[2])
resource = String(parts[3])
ext = String(parts[4])
rand = String(parts[5])
counter = String(parts[6])
}
}

View File

@@ -0,0 +1,36 @@
import Capacitor
import Foundation
@objc(IntelligentsPlugin)
public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "IntelligentsPlugin"
public let jsName = "Intelligents"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise),
]
public private(set) weak var representController: UIViewController?
init(representController: UIViewController) {
self.representController = representController
super.init()
}
deinit {
representController = nil
}
@objc public func presentIntelligentsButton(_ call: CAPPluginCall) {
DispatchQueue.main.async {
self.representController?.presentIntelligentsButton()
call.resolve()
}
}
@objc public func dismissIntelligentsButton(_ call: CAPPluginCall) {
DispatchQueue.main.async {
self.representController?.dismissIntelligentsButton()
call.resolve()
}
}
}

View File

@@ -0,0 +1,38 @@
//
// RootViewController.swift
// App
//
// Created by on 2024/11/18.
//
import UIKit
@objc
class RootViewController: UINavigationController {
override init(rootViewController _: UIViewController) {
fatalError() // "you are not allowed to call this"
}
override init(navigationBarClass _: AnyClass?, toolbarClass _: AnyClass?) {
fatalError() // "you are not allowed to call this"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commitInit()
}
override init(nibName _: String?, bundle _: Bundle?) {
fatalError() // "you are not allowed to call this"
}
func commitInit() {
assert(viewControllers.isEmpty)
viewControllers = [AFFiNEViewController()]
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
}

View File

@@ -1,107 +0,0 @@
import Capacitor
import CryptoSwift
@objc(HashcashPlugin)
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "HashcashPlugin"
public let jsName = "Hashcash"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise)
]
@objc func hash(_ call: CAPPluginCall) {
DispatchQueue.global(qos: .default).async {
let challenge = call.getString("challenge") ?? ""
let bits = call.getInt("bits") ?? 20;
call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()])
}
}
}
let SALT_LENGTH = 16
struct Stamp {
let version: String
let claim: UInt32
let ts: String
let resource: String
let ext: String
let rand: String
let counter: String
func checkExpiration() -> Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
guard let date = dateFormatter.date(from: ts) else { return false }
return Date().addingTimeInterval(5 * 60) <= date
}
func check(bits: UInt32, resource: String) -> Bool {
if version == "1" && bits <= claim && checkExpiration() && self.resource == resource {
let hexDigits = Int(floor(Float(claim) / 4.0))
// Check challenge
let formatted = format()
let result = formatted.data(using: .utf8)!.sha3(.sha256).compactMap { String(format: "%02x", $0) }.joined()
return result.prefix(hexDigits) == String(repeating: "0", count: hexDigits)
} else {
return false
}
}
func format() -> String {
return "\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
}
static func mint(resource: String, bits: UInt32? = nil) -> Stamp {
let version = "1"
let now = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
let ts = dateFormatter.string(from: now)
let bits = bits ?? 20
let rand = String((0..<SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
var counter = 0
var counterHex = ""
var hasher = SHA3(variant: .sha256)
while true {
let toHash = "\(challenge):\(String(format: "%x", counter))"
let hashed = try! hasher.finish(withBytes: toHash.data(using: .utf8)!.bytes)
let result = hashed.compactMap { String(format: "%02x", $0) }.joined()
if result.prefix(hexDigits) == zeros {
counterHex = String(format: "%x", counter)
break
}
counter += 1
}
return Stamp(version: version, claim: bits, ts: ts, resource: resource, ext: "", rand: rand, counter: counterHex)
}
}
extension Stamp {
init?(from string: String) throws {
let parts = string.split(separator: ":")
guard parts.count == 7 else {
throw NSError(domain: "StampError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp, expected 7 parts, got \(parts.count)"])
}
guard let claim = UInt32(parts[1]) else {
throw NSError(domain: "StampError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp"])
}
self.version = String(parts[0])
self.claim = claim
self.ts = String(parts[2])
self.resource = String(parts[3])
self.ext = String(parts[4])
self.rand = String(parts[5])
self.counter = String(parts[6])
}
}

View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,14 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Intelligents",
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
],
targets: [
.target(name: "Intelligents"),
]
)

View File

@@ -0,0 +1,4 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
class Intelligents {}

View File

@@ -0,0 +1,13 @@
//
// IntelligentsButton.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
// floating button to open intelligent panel
class IntelligentsButton: UIView {
let image = UIImageView()
}

View File

@@ -0,0 +1,14 @@
//
// IntelligentsChatController.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
class IntelligentsChatController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}

View File

@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
COCOAPODS: 1.16.2
COCOAPODS: 1.15.2

View File

@@ -3,6 +3,7 @@ import { AppContainer } from '@affine/core/desktop/components/app-container';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
import { AIButtonProvider } from '@affine/core/modules/ai-button';
import {
AuthService,
ValidatorProvider,
@@ -18,13 +19,22 @@ import {
configureBrowserWorkspaceFlavours,
configureIndexedDBWorkspaceEngineStorageProvider,
} from '@affine/core/modules/workspace-engine';
import {
docLinkBaseURLMiddleware,
MarkdownAdapter,
titleMiddleware,
} from '@blocksuite/affine/blocks';
import { Job } from '@blocksuite/affine/store';
import { App as CapacitorApp } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
import {
DocsService,
Framework,
FrameworkRoot,
getCurrentStore,
GlobalContextService,
LifecycleService,
WorkspacesService,
} from '@toeverything/infra';
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
@@ -32,6 +42,7 @@ import { RouterProvider } from 'react-router-dom';
import { configureFetchProvider } from './fetch';
import { Cookie } from './plugins/cookie';
import { Hashcash } from './plugins/hashcash';
import { Intelligents } from './plugins/intelligents';
const future = {
v7_startTransition: true,
@@ -76,8 +87,63 @@ framework.impl(ValidatorProvider, {
return res.value;
},
});
framework.impl(AIButtonProvider, {
presentAIButton: () => {
return Intelligents.presentIntelligentsButton();
},
dismissAIButton: () => {
return Intelligents.dismissIntelligentsButton();
},
});
const frameworkProvider = framework.provider();
(window as any).getCurrentDocContentInMarkdown = async () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
const currentWorkspaceId =
globalContextService.globalContext.workspaceId.get();
const currentDocId = globalContextService.globalContext.docId.get();
const workspacesService = frameworkProvider.get(WorkspacesService);
const workspaceRef = currentWorkspaceId
? workspacesService.openByWorkspaceId(currentWorkspaceId)
: null;
if (!workspaceRef) {
return;
}
const { workspace, dispose: disposeWorkspace } = workspaceRef;
const docsService = workspace.scope.get(DocsService);
const docRef = currentDocId ? docsService.open(currentDocId) : null;
if (!docRef) {
return;
}
const { doc, release: disposeDoc } = docRef;
try {
const blockSuiteDoc = doc.blockSuiteDoc;
const job = new Job({
collection: blockSuiteDoc.collection,
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
});
const snapshot = await job.docToSnapshot(blockSuiteDoc);
const adapter = new MarkdownAdapter(job);
if (!snapshot) {
return;
}
const markdownResult = await adapter.fromDocSnapshot({
snapshot,
assets: job.assetsManager,
});
return markdownResult.file;
} finally {
disposeDoc();
disposeWorkspace();
}
};
// setup application lifecycle events, and emit application start event
window.addEventListener('focus', () => {
frameworkProvider.get(LifecycleService).applicationFocus();

View File

@@ -0,0 +1,4 @@
export interface IntelligentsPlugin {
presentIntelligentsButton(): Promise<void>;
dismissIntelligentsButton(): Promise<void>;
}

View File

@@ -0,0 +1,8 @@
import { registerPlugin } from '@capacitor/core';
import type { IntelligentsPlugin } from './definitions';
const Intelligents = registerPlugin<IntelligentsPlugin>('Intelligents');
export * from './definitions';
export { Intelligents };

View File

@@ -10,6 +10,7 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
import { PageHeader } from '@affine/core/components/mobile';
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
import { AIButtonService } from '@affine/core/modules/ai-button';
import { EditorService } from '@affine/core/modules/editor';
import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchService } from '@affine/core/modules/workbench';
@@ -56,6 +57,7 @@ const DetailPageImpl = () => {
workspaceService,
globalContextService,
featureFlagService,
aIButtonService,
} = useServices({
WorkbenchService,
ViewService,
@@ -64,6 +66,7 @@ const DetailPageImpl = () => {
WorkspaceService,
GlobalContextService,
FeatureFlagService,
AIButtonService,
});
const editor = editorService.editor;
const workspace = workspaceService.workspace;
@@ -108,6 +111,14 @@ const DetailPageImpl = () => {
};
}, [doc, globalContext, mode]);
useEffect(() => {
aIButtonService.presentAIButton(true);
return () => {
aIButtonService.presentAIButton(false);
};
}, [aIButtonService]);
useEffect(() => {
if (!enableKeyboardToolbar) setDocReadonly(doc.id, true);
}, [enableKeyboardToolbar, doc.id, setDocReadonly]);

View File

@@ -0,0 +1,13 @@
export { AIButtonProvider } from './provider/ai-button';
export { AIButtonService } from './services/ai-button';
import type { Framework } from '@toeverything/infra';
import { AIButtonProvider } from './provider/ai-button';
import { AIButtonService } from './services/ai-button';
export const configureAIButtonModule = (framework: Framework) => {
framework.service(AIButtonService, container => {
return new AIButtonService(container.getOptional(AIButtonProvider));
});
};

View File

@@ -0,0 +1,9 @@
import { createIdentifier } from '@toeverything/infra';
export interface AIButtonProvider {
presentAIButton: () => Promise<void>;
dismissAIButton: () => Promise<void>;
}
export const AIButtonProvider =
createIdentifier<AIButtonProvider>('AIButtonProvider');

View File

@@ -0,0 +1,48 @@
import { DebugLogger } from '@affine/debug';
import {
effect,
exhaustMapWithTrailing,
fromPromise,
Service,
} from '@toeverything/infra';
import {
catchError,
distinctUntilChanged,
EMPTY,
mergeMap,
throttleTime,
} from 'rxjs';
import type { AIButtonProvider } from '../provider/ai-button';
const logger = new DebugLogger('AIButtonService');
export class AIButtonService extends Service {
constructor(private readonly aiButtonProvider?: AIButtonProvider) {
super();
}
presentAIButton = effect(
distinctUntilChanged(),
throttleTime<boolean>(1000), // throttle time to avoid frequent calls
exhaustMapWithTrailing((present: boolean) => {
return fromPromise(async () => {
if (!this.aiButtonProvider) {
return;
}
if (present) {
await this.aiButtonProvider.presentAIButton();
} else {
await this.aiButtonProvider.dismissAIButton();
}
return;
}).pipe(
mergeMap(() => EMPTY),
catchError(err => {
logger.error('presentAIButton error', err);
return EMPTY;
})
);
})
);
}

View File

@@ -1,6 +1,7 @@
import { configureQuotaModule } from '@affine/core/modules/quota';
import { configureInfraModules, type Framework } from '@toeverything/infra';
import { configureAIButtonModule } from './ai-button';
import { configureAppSidebarModule } from './app-sidebar';
import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection';
@@ -67,4 +68,5 @@ export function configureCommonModules(framework: Framework) {
configureDialogModule(framework);
configureDocInfoModule(framework);
configureOpenInApp(framework);
configureAIButtonModule(framework);
}