diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index 00131c3fd..9555d7b35 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "1.1.6" } }, - { - "identity" : "litext", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/Litext", - "state" : { - "revision" : "c37f3ab5826659854311e20d6c3942d4905b00b6", - "version" : "0.5.0" - } - }, { "identity" : "lrucache", "kind" : "remoteSourceControl", @@ -50,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Lakr233/MarkdownView", "state" : { - "revision" : "29a9da19d6dc21af4e629c423961b0f453ffe192", - "version" : "2.3.8" + "revision" : "446dba45be81c67d0717d19277367dcbe5b2fb12", + "version" : "3.1.9" } }, { @@ -68,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Lakr233/Splash", "state" : { - "revision" : "4d997712fe07f75695aacdf287aeb3b1f2c6ab88", - "version" : "0.17.0" + "revision" : "de9cde249fdb7a173a6e6b950ab18b11f6c2a557", + "version" : "0.18.0" } }, { diff --git a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift index f6c2dec22..c26f57391 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift @@ -13,23 +13,5 @@ extension AFFiNEViewController: IntelligentsButtonDelegate { // if it shows up then we are ready to go let controller = IntelligentsController() self.present(controller, animated: true) - // IntelligentContext.shared.webView = webView - // button.beginProgress() - // IntelligentContext.shared.preparePresent { result in - // DispatchQueue.main.async { - // button.stopProgress() - // switch result { - // case .success: - // case let .failure(failure): - // let alert = UIAlertController( - // title: "Error", - // message: failure.localizedDescription, - // preferredStyle: .alert - // ) - // alert.addAction(UIAlertAction(title: "OK", style: .default)) - // self.present(alert, animated: true) - // } - // } - // } } } diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index 54c6fcf1e..d9adbc658 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -64,12 +64,7 @@ class AFFiNEViewController: CAPBridgeViewController { switch result { case .failure: break case .success: - #if DEBUG - // only show the button in debug mode before we get done self.presentIntelligentsButton() - #else - break - #endif } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotRecentSessionsQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotRecentSessionsQuery.graphql.swift index 3ca0ae6a0..ca0f2f173 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotRecentSessionsQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotRecentSessionsQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery { public static let operationName: String = "getCopilotRecentSessions" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: true } ) { __typename ...PaginatedCopilotChats } } } }"#, + #"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#, fragments: [CopilotChatHistory.self, CopilotChatMessage.self, PaginatedCopilotChats.self] )) @@ -69,7 +69,7 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery { "options": [ "fork": false, "sessionOrder": "desc", - "withMessages": true + "withMessages": false ] ]), ] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetWorkspacePageByIdQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetWorkspacePageByIdQuery.graphql.swift index 1ab701e5c..6c07dd844 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetWorkspacePageByIdQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetWorkspacePageByIdQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery { public static let operationName: String = "getWorkspacePageById" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public } } }"# + #"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public title summary } } }"# )) public var workspaceId: String @@ -68,12 +68,16 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery { .field("mode", GraphQLEnum.self), .field("defaultRole", GraphQLEnum.self), .field("public", Bool.self), + .field("title", String?.self), + .field("summary", String?.self), ] } public var id: String { __data["id"] } public var mode: GraphQLEnum { __data["mode"] } public var defaultRole: GraphQLEnum { __data["defaultRole"] } public var `public`: Bool { __data["public"] } + public var title: String? { __data["title"] } + public var summary: String? { __data["summary"] } } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index 96c4d5b1b..a0fade8c2 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"), .package(url: "https://github.com/Recouse/EventSource", from: "0.1.4"), .package(url: "https://github.com/Lakr233/ListViewKit", from: "1.1.6"), - .package(url: "https://github.com/Lakr233/MarkdownView", exact: "2.3.8"), + .package(url: "https://github.com/Lakr233/MarkdownView", from: "3.1.9"), ], targets: [ .target(name: "Intelligents", dependencies: [ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+CURD.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+CURD.swift new file mode 100644 index 000000000..afc3e9c30 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+CURD.swift @@ -0,0 +1,33 @@ +// +// ChatManager+CURD.swift +// Intelligents +// +// Created by 秋星桥 on 7/14/25. +// + +import AffineGraphQL +import Apollo +import ApolloAPI +import EventSource +import Foundation +import MarkdownParser +import MarkdownView + +extension ChatManager { + func clearCurrentSession() { + guard let session = IntelligentContext.shared.currentSession else { + print("[-] no current session to clear") + return + } + + let mutation = CleanupCopilotSessionMutation(input: .init( + docId: session.docId ?? "", + sessionIds: [session.id], + workspaceId: session.workspaceId + )) + + QLService.shared.client.perform(mutation: mutation) { result in + print("[+] cleanup session result: \(result)") + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift index 2298e7279..e5c571ce4 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift @@ -24,53 +24,173 @@ private extension InputBoxData { } } -extension ChatManager { - public func startUserRequest( - content: String, - inputBoxData: InputBoxData, - sessionId: String - ) { +public extension ChatManager { + func startUserRequest(editorData: InputBoxData, sessionId: String) { append(sessionId: sessionId, UserMessageCellViewModel( id: .init(), - content: inputBoxData.text, + content: editorData.text, timestamp: .init() )) append(sessionId: sessionId, UserHintCellViewModel( id: .init(), timestamp: .init(), - imageAttachments: inputBoxData.imageAttachments, - fileAttachments: inputBoxData.fileAttachments, - docAttachments: inputBoxData.documentAttachments + imageAttachments: editorData.imageAttachments, + fileAttachments: editorData.fileAttachments, + docAttachments: editorData.documentAttachments )) + let viewModelId = append(sessionId: sessionId, AssistantMessageCellViewModel( + id: .init(), + content: "...", + timestamp: .init() + )) + scrollToBottomPublisher.send(sessionId) + + guard let workspaceId = IntelligentContext.shared.currentWorkspaceId, + !workspaceId.isEmpty + else { + report(sessionId, ChatError.unknownError) + assertionFailure("Invalid workspace ID") + return + } + + DispatchQueue.global().async { + self.prepareContext( + workspaceId: workspaceId, + sessionId: sessionId, + editorData: editorData, + viewModelId: viewModelId + ) + } + } +} + +private extension ChatManager { + func prepareContext( + workspaceId: String, + sessionId: String, + editorData: InputBoxData, + viewModelId: UUID + ) { + assert(!Thread.isMainThread) + let createContext = CreateCopilotContextMutation( + workspaceId: workspaceId, + sessionId: sessionId + ) + QLService.shared.client.perform(mutation: createContext) { result in + DispatchQueue.main.async { + switch result { + case let .success(graphQLResult): + guard let contextId = graphQLResult.data?.createCopilotContext else { + self.report(sessionId, ChatError.invalidResponse) + return + } + print("[+] copilot context created: \(contextId)") + + DispatchQueue.global().async { + let docAttachGroup = DispatchGroup() + for docAttach in editorData.documentAttachments { + let addDoc = AddContextDocMutation( + options: .init( + contextId: contextId, + docId: docAttach.documentID + ) + ) + docAttachGroup.enter() + QLService.shared.client.perform(mutation: addDoc) { result in + switch result { + case .success: + print("[+] doc \(docAttach.documentID) added to context") + case let .failure(error): + print("[-] addContextDoc failed: \(error)") + } + docAttachGroup.leave() + } + } + + docAttachGroup.notify(queue: .global()) { + var contextSnippet = "" + if !editorData.documentAttachments.isEmpty { + let sem = DispatchSemaphore(value: 0) + let matchQuery = MatchContextQuery( + contextId: .some(contextId), + workspaceId: .some(workspaceId), + content: editorData.text, + limit: .none, + scopedThreshold: .none, + threshold: .none + ) + QLService.shared.client.fetch(query: matchQuery) { result in + switch result { + case let .success(queryResult): + let matches = queryResult.data?.currentUser?.copilot.contexts ?? [] + let matchDocs = matches.compactMap(\.matchWorkspaceDocs).flatMap(\.self) + for context in matchDocs { + contextSnippet += "\(context.content)\n" + } + case let .failure(error): + print("[-] matchContext failed: \(error)") + // self.report(sessionId, error) + } + sem.signal() + } + sem.wait() + } + print("[+] context snippet prepared: \(contextSnippet)") + self.startCopilotResponse( + editorData: editorData, + contextSnippet: contextSnippet, + sessionId: sessionId, + viewModelId: viewModelId + ) + } + } + case let .failure(error): + self.report(sessionId, error) + return + } + } + } + } + + func startCopilotResponse( + editorData: InputBoxData, + contextSnippet: String, + sessionId: String, + viewModelId: UUID + ) { + assert(!Thread.isMainThread) + let messageParameters: [String: AnyHashable] = [ // packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx - "docs": inputBoxData.documentAttachments.map(\.documentID), // affine doc + "docs": editorData.documentAttachments.map(\.documentID), // affine doc "files": [String](), // attachment in context, keep nil for now - "searchMode": inputBoxData.isSearchEnabled ? "MUST" : "AUTO", + "searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO", ] - let uploadableAttachments: [GraphQLFile] = [ - inputBoxData.fileAttachments.map { file -> GraphQLFile in - .init( - fieldName: file.name, - originalName: file.name, - data: file.data ?? .init() - ) + let attachmentFieldName = "options.blobs" + var uploadableAttachments: [GraphQLFile] = [ + editorData.fileAttachments.map { file -> GraphQLFile in + .init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init()) }, - inputBoxData.imageAttachments.map { image -> GraphQLFile in - .init( - fieldName: image.hashValue.description, - originalName: "image.jpg", - data: image.imageData - ) + editorData.imageAttachments.map { image -> GraphQLFile in + .init(fieldName: attachmentFieldName, originalName: "image.jpg", data: image.imageData) }, ].flatMap(\.self) assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) }) + // in Apollo, filed name is handled as attached object to field when there is only one attachment + // to use array on our server, we need to append a dummy attachment + // which is ignored if data is empty and name is empty + if uploadableAttachments.count == 1 { + uploadableAttachments.append(.init(fieldName: attachmentFieldName, originalName: "", data: .init())) + } guard let input = try? CreateChatMessageInput( - content: .some(content), + attachments: [], + blobs: .some([]), // must have the placeholder + content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"), params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)), sessionId: sessionId ) else { + report(sessionId, ChatError.unknownError) assertionFailure() // very unlikely to happen return } @@ -83,11 +203,6 @@ extension ChatManager { self.report(sessionId, ChatError.invalidResponse) return } - let viewModelId = self.append(sessionId: sessionId, AssistantMessageCellViewModel( - id: .init(), - content: .init(), - timestamp: .init() - )) self.startStreamingResponse( sessionId: sessionId, messageId: messageIdentifier, @@ -99,8 +214,10 @@ extension ChatManager { } } } +} - private func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) { +private extension ChatManager { + func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) { let base = IntelligentContext.shared.webViewMetadata[.currentServerBaseUrl] as? String guard let base, let url = URL(string: base) else { report(sessionId, ChatError.invalidServerConfiguration) @@ -164,24 +281,11 @@ extension ChatManager { vmId: UUID ) { let result = MarkdownParser().parse(document) - var renderedContexts: [String: RenderedItem] = [:] - for (key, value) in result.mathContext { - let image = MathRenderer.renderToImage( - latex: value, - fontSize: MarkdownTheme.default.fonts.body.pointSize, - textColor: MarkdownTheme.default.colors.body - )?.withRenderingMode(.alwaysTemplate) - let renderedContext = RenderedItem( - image: image, - text: value - ) - renderedContexts["math://\(key)"] = renderedContext - } + let content = MarkdownTextView.PreprocessContent(parserResult: result, theme: .default) with(sessionId: sessionId, vmId: vmId) { (viewModel: inout AssistantMessageCellViewModel) in viewModel.content = document - viewModel.documentBlocks = result.document - viewModel.documentRenderedContent = renderedContexts + viewModel.preprocessedContent = content } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager.swift index 3a9c8649d..cc63c25b0 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager.swift @@ -33,6 +33,12 @@ public class ChatManager: ObservableObject, @unchecked Sendable { closable.removeAll() } + public func clearAll() { + assert(Thread.isMainThread) + closeAll() + viewModels.removeAll() + } + public func with(sessionId: String, _ action: (inout OrderedDictionary) -> Void) { if Thread.isMainThread { if var sessionViewModels = viewModels[sessionId] { @@ -59,8 +65,6 @@ public class ChatManager: ObservableObject, @unchecked Sendable { return } sessionViewModels[vmId] = vm - } else { - assertionFailure() } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift index c9f95cefe..49ecd4e56 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift @@ -85,11 +85,7 @@ extension MainViewController: InputBoxDelegate { } ChatManager.shared.closeAll() - ChatManager.shared.startUserRequest( - content: inputData.text, - inputBoxData: inputData, - sessionId: currentSession.id - ) + ChatManager.shared.startUserRequest(editorData: inputData, sessionId: currentSession.id) } private func showAlert(title: String, message: String) { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift index 24d3e3227..b0b8b8844 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift @@ -21,11 +21,6 @@ class AssistantMessageCell: ChatBaseCell { contentView.addSubview(markdownView) } - override func prepareForReuse() { - super.prepareForReuse() - markdownView.prepareForReuse() - } - override func configure(with viewModel: any ChatCellViewModel) { super.configure(with: viewModel) @@ -33,10 +28,7 @@ class AssistantMessageCell: ChatBaseCell { assertionFailure() return } - markdownView.setMarkdown( - vm.documentBlocks, - renderedContent: vm.documentRenderedContent - ) + markdownView.setMarkdown(vm.preprocessedContent) } override func layoutContentView(bounds: CGRect) { @@ -53,10 +45,7 @@ class AssistantMessageCell: ChatBaseCell { markdownViewForSizeCalculation.frame = .init( x: 0, y: 0, width: width, height: .greatestFiniteMagnitude ) - markdownViewForSizeCalculation.setMarkdown( - vm.documentBlocks, - renderedContent: vm.documentRenderedContent - ) + markdownViewForSizeCalculation.setMarkdownManually(vm.preprocessedContent) let boundingSize = markdownViewForSizeCalculation.boundingSize(for: width) return ceil(boundingSize.height) } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ViewModel/CCVM+Assistant.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ViewModel/CCVM+Assistant.swift index 4cf2ff939..54068185b 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ViewModel/CCVM+Assistant.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ViewModel/CCVM+Assistant.swift @@ -38,8 +38,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel { var citations: [CitationViewModel]? var actions: [MessageActionViewModel]? - var documentBlocks: [MarkdownBlockNode] - var documentRenderedContent: RenderContext + var preprocessedContent: MarkdownTextView.PreprocessContent init( id: UUID, @@ -53,7 +52,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel { actions: [MessageActionViewModel]? = nil ) { // time expensive rendering should not happen here - assert(!Thread.isMainThread || content.isEmpty) + assert(!Thread.isMainThread || content.count < 10) // allow placeholder content self.id = id self.content = content @@ -67,21 +66,10 @@ struct AssistantMessageCellViewModel: ChatCellViewModel { let parser = MarkdownParser() let parserResult = parser.parse(content) - documentBlocks = parserResult.document - var renderedContexts: [String: RenderedItem] = [:] - for (key, value) in parserResult.mathContext { - let image = MathRenderer.renderToImage( - latex: value, - fontSize: MarkdownTheme.default.fonts.body.pointSize, - textColor: MarkdownTheme.default.colors.body - )?.withRenderingMode(.alwaysTemplate) - let renderedContext = RenderedItem( - image: image, - text: value - ) - renderedContexts["math://\(key)"] = renderedContext - } - documentRenderedContent = renderedContexts + preprocessedContent = MarkdownTextView.PreprocessContent( + parserResult: parserResult, + theme: .default, + ) } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift index 549e59f03..fb0188b70 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift @@ -6,8 +6,8 @@ private let unselectedColor: UIColor = .affineIconPrimary private let selectedColor: UIColor = .affineIconActivated private let configurableOptions: [ConfigurableOptions] = [ - .networking, - .reasoning, +// .networking, +// .reasoning, ] enum ConfigurableOptions { case tool diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift index 6592051e3..031cf5c34 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift @@ -22,11 +22,20 @@ class MainHeaderView: UIView { $0.textAlignment = .center } + private lazy var modelMenu = UIDeferredMenuElement.uncached { completion in + completion([]) + } + private lazy var dropdownButton = UIButton(type: .system).then { $0.imageView?.contentMode = .scaleAspectFit $0.setImage(UIImage.affineArrowDown, for: .normal) $0.tintColor = UIColor.affineIconPrimary $0.addTarget(self, action: #selector(dropdownButtonTapped), for: .touchUpInside) + $0.showsMenuAsPrimaryAction = true + $0.menu = UIMenu(options: [.displayInline], children: [ + modelMenu, + ]) + $0.isHidden = true } private lazy var centerStackView = UIStackView().then { @@ -45,6 +54,13 @@ class MainHeaderView: UIView { $0.layer.cornerRadius = 8 $0.addTarget(self, action: #selector(menuButtonTapped), for: .touchUpInside) $0.setContentHuggingPriority(.required, for: .horizontal) + $0.showsMenuAsPrimaryAction = true + $0.menu = .init(options: [.displayInline], children: [ + UIAction(title: "Clear History", image: .affineBroom, handler: { _ in + ChatManager.shared.clearCurrentSession() + ChatManager.shared.clearAll() + }), + ]) } private lazy var leftSpacerView = UIView()