Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public enum AIChatURLParameters {
public static let voiceModeValue = "voice"
public static let imageModeValue = "image"

public static let sidebarName = "sidebar"
public static let sidebarOpenValue = "open"

/// Appends `?mode=voice` to the given base URL.
public static func voiceModeURL(from baseURL: URL) -> URL {
modeURL(from: baseURL, mode: voiceModeValue)
Expand All @@ -46,6 +49,11 @@ public enum AIChatURLParameters {
modeURL(from: baseURL, mode: imageModeValue)
}

/// Appends `?sidebar=open` to the given base URL.
public static func sidebarOpenURL(from baseURL: URL) -> URL {
baseURL.addingOrReplacing(URLQueryItem(name: sidebarName, value: sidebarOpenValue))
}

private static func modeURL(from baseURL: URL, mode: String) -> URL {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
return baseURL
Expand Down
10 changes: 9 additions & 1 deletion SharedPackages/AIChat/Sources/AIChat/Shared/URL+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ extension URL {
- Parameter queryItem: The query item to add or replace.
- Returns: A new URL with the query item added or replaced, or the original URL if the query item's value is invalid.
*/
func addingOrReplacing(_ queryItem: URLQueryItem) -> URL {
public func addingOrReplacing(_ queryItem: URLQueryItem) -> URL {
guard let queryValue = queryItem.value,
!queryValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return self
Expand Down Expand Up @@ -85,6 +85,14 @@ extension URL {
} == true
}

/// Returns `true` if the URL requests the Duck AI sidebar to be open on load (`?sidebar=open`).
public var isDuckAISidebarOpen: Bool {
guard isDuckAIURL else { return false }
return queryItems?.contains {
$0.name == AIChatURLParameters.sidebarName && $0.value == AIChatURLParameters.sidebarOpenValue
} == true
}

/// Returns the chat ID from the URL if present, or nil if not a Duck AI URL with a chat ID.
public var duckAIChatID: String? {
guard isDuckAIURL,
Expand Down
24 changes: 1 addition & 23 deletions iOS/DuckDuckGo/AIChat/AIChatContentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,6 @@ final class AIChatContentHandler: AIChatContentHandling {
private let statisticsLoader: StatisticsLoader

private var userScript: AIChatUserScriptProviding?
private var isFrontendReady = false {
didSet {
if isFrontendReady { flushPendingActions() }
}
}
private var pendingSidebarToggle = false

/// Closure to get page context for contextual mode. Nil in full mode.
/// Parameter is the request reason (e.g., `.userAction` for manual attach).
Expand Down Expand Up @@ -237,26 +231,14 @@ final class AIChatContentHandler: AIChatContentHandling {
}

/// Submits a toggle sidebar action to open/close the sidebar.
/// If the frontend isn't ready yet, queues the action until it initializes.
func submitToggleSidebarAction() {
if isFrontendReady {
userScript?.submitToggleSidebarAction()
} else {
pendingSidebarToggle = true
}
userScript?.submitToggleSidebarAction()
}

func submitPageContext(_ context: AIChatPageContextData?) {
userScript?.submitPageContext(context)
}

private func flushPendingActions() {
if pendingSidebarToggle {
pendingSidebarToggle = false
userScript?.submitToggleSidebarAction()
}
}

/// Fires AI Chat telemetry: product surface telemetry, 'chat open' pixel, and sets the AI Chat feature as 'used before'
func fireAIChatTelemetry() {
productSurfaceTelemetry.duckAIUsed()
Expand All @@ -273,10 +255,6 @@ extension AIChatContentHandler: AIChatUserScriptDelegate {
delegate?.aiChatContentHandlerDidReceivePageContextRequest(self)
}

if message == .setAIChatHistoryEnabled {
isFrontendReady = true
}

switch message {
case .openAIChatSettings:
delegate?.aiChatContentHandlerDidReceiveOpenSettingsRequest(self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protocol AIChatContextualSheetCoordinatorDelegate: AnyObject {
func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestToLoad url: URL)

/// Called when the user taps expand to open duck.ai in a new tab with the given chat URL.
func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL, shouldToggleSidebar: Bool)
func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL)

/// Called when the user requests to open AI Chat settings.
func aiChatContextualSheetCoordinatorDidRequestOpenSettings(_ coordinator: AIChatContextualSheetCoordinator)
Expand Down Expand Up @@ -344,8 +344,8 @@ extension AIChatContextualSheetCoordinator: AIChatContextualSheetViewControllerD
}
}

func aiChatContextualSheetViewController(_ viewController: AIChatContextualSheetViewController, didRequestExpandWithURL url: URL, shouldToggleSidebar: Bool) {
delegate?.aiChatContextualSheetCoordinator(self, didRequestExpandWithURL: url, shouldToggleSidebar: shouldToggleSidebar)
func aiChatContextualSheetViewController(_ viewController: AIChatContextualSheetViewController, didRequestExpandWithURL url: URL) {
delegate?.aiChatContextualSheetCoordinator(self, didRequestExpandWithURL: url)
viewController.dismiss(animated: true) { [weak self] in
self?.startSessionTimer()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protocol AIChatContextualSheetViewControllerDelegate: AnyObject {
func aiChatContextualSheetViewControllerDidRequestDismiss(_ viewController: AIChatContextualSheetViewController)

/// Called when the user taps expand to open duck.ai in a new tab with the current chat URL
func aiChatContextualSheetViewController(_ viewController: AIChatContextualSheetViewController, didRequestExpandWithURL url: URL, shouldToggleSidebar: Bool)
func aiChatContextualSheetViewController(_ viewController: AIChatContextualSheetViewController, didRequestExpandWithURL url: URL)

/// Called when the user requests to open AI Chat settings
func aiChatContextualSheetViewControllerDidRequestOpenSettings(_ viewController: AIChatContextualSheetViewController)
Expand Down Expand Up @@ -377,7 +377,7 @@ final class AIChatContextualSheetViewController: UIViewController {
pixelHandler.fireExpandButtonTapped()
let url = sessionState.contextualChatURL ?? aiChatSettings.aiChatURL
Logger.aiChat.debug("[AIChatContextual] Expand tapped with URL: \(url.absoluteString)")
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url, shouldToggleSidebar: false)
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url)
}

@objc private func fireButtonTapped() {
Expand Down Expand Up @@ -797,14 +797,14 @@ extension AIChatContextualSheetViewController: AIChatRecentChatsPopupViewModelDe
dismissRecentChatsPopup()
pixelHandler.fireRecentChatSelected()
let url = aiChatSettings.aiChatURL.withChatID(chat.chatId)
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url, shouldToggleSidebar: false)
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url)
}

func recentChatsPopupDidSelectViewAll() {
dismissRecentChatsPopup()
pixelHandler.fireViewAllChatsTapped()
let url = aiChatSettings.aiChatURL
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url, shouldToggleSidebar: true)
let url = AIChatURLParameters.sidebarOpenURL(from: aiChatSettings.aiChatURL)
delegate?.aiChatContextualSheetViewController(self, didRequestExpandWithURL: url)
}

func recentChatsPopupDidDismiss() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ final class AIChatRecentChatsPopupViewController: UIViewController {

NSLayoutConstraint.activate([
shadowContainer.topAnchor.constraint(equalTo: view.topAnchor, constant: cardTop),
shadowContainer.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor),
Comment thread
cursor[bot] marked this conversation as resolved.
shadowContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: cardLeading),
shadowContainer.widthAnchor.constraint(equalToConstant: Constants.popupWidth),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,8 @@ extension TabViewController: AIChatContextualSheetCoordinatorDelegate {
delegate?.tab(self, didRequestNewTabForUrl: url, openedByPage: false, inheritingAttribution: nil)
}

func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL, shouldToggleSidebar: Bool) {
func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL) {
delegate?.tab(self, didRequestNewTabForUrl: url, openedByPage: false, inheritingAttribution: nil)
if shouldToggleSidebar {
Comment thread
cursor[bot] marked this conversation as resolved.
delegate?.tabDidRequestToggleSidebarOnCurrentTab(self)
}
}

func aiChatContextualSheetCoordinatorDidRequestOpenSettings(_ coordinator: AIChatContextualSheetCoordinator) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,9 @@ private extension MainViewController {
let hasExistingChat = tab.url?.duckAIChatID != nil
let tabURL = tab.url ?? tab.link?.url
let isVoiceMode = tabURL?.isDuckAIVoiceMode == true || tab.isVoiceModeRequested
let isSidebarOpen = tabURL?.isDuckAISidebarOpen == true
tab.isVoiceModeRequested = false
let shouldExpandAfterRefresh = !hasExistingChat && !coordinator.hasSubmittedPrompt && !isVoiceMode
let shouldExpandAfterRefresh = !hasExistingChat && !coordinator.hasSubmittedPrompt && !isVoiceMode && !isSidebarOpen
return .refreshAITab(.showCollapsed(expandAfterRefresh: shouldExpandAfterRefresh))
}

Expand Down
2 changes: 1 addition & 1 deletion iOS/DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2166,7 +2166,7 @@ public struct UserText {
// MARK: - AI Chat Quick Actions
public static let aiChatQuickActionAskAboutPage = NotLocalizedString("duckai.quick.action.ask.about.page", value: "Ask about page", comment: "Title for the ask about page quick action chip in Duck.ai contextual sheet. Tapping attaches the current page content.")
public static let aiChatQuickActionSummarize = NSLocalizedString("duckai.quick.action.summarize", value: "Summarize This Page", comment: "Title for the summarize quick action chip in Duck.ai contextual sheet")
public static let aiChatQuickActionSummarizePage = NotLocalizedString("duckai.quick.action.summarize.page", value: "Summarize Page", comment: "Title for the summarize page quick action chip in the improved Duck.ai contextual sheet")
public static let aiChatQuickActionSummarizePage = NotLocalizedString("duckai.quick.action.summarize.page", value: "Summarize page", comment: "Title for the summarize page quick action chip in the improved Duck.ai contextual sheet")
public static let aiChatQuickActionAttach = NSLocalizedString("duckai.quick.action.attach", value: "Attach Page Content", comment: "Title for the attach page content quick action chip in Duck.ai contextual sheet")

// MARK: - AI Chat Recent Chats Popup
Expand Down
10 changes: 2 additions & 8 deletions iOS/DuckDuckGoTests/AIChat/AIChatContentHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ final class AIChatContentHandlerTests: XCTestCase {
XCTAssertEqual(mockUserScript.submitToggleSidebarActionCallCount, 1)
}

func testSubmitToggleSidebarActionQueuesWhenFrontendNotReady() throws {
func testSubmitToggleSidebarActionCallsThroughDirectly() throws {
// Given
let mockUserScript = MockAIChatUserScript()
let mockWebView = WKWebView()
Expand All @@ -396,13 +396,7 @@ final class AIChatContentHandlerTests: XCTestCase {
// When
handler.submitToggleSidebarAction()

// Then - action is queued, not called immediately
XCTAssertEqual(mockUserScript.submitToggleSidebarActionCallCount, 0)

// When - frontend becomes ready
handler.aiChatUserScript(makeTestUserScript(), didReceiveMessage: .setAIChatHistoryEnabled)

// Then - queued action is flushed
// Then
XCTAssertEqual(mockUserScript.submitToggleSidebarActionCallCount, 1)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,15 @@ final class AIChatContextualSheetCoordinatorTests: XCTestCase {
private final class MockDelegate: AIChatContextualSheetCoordinatorDelegate {
var didRequestToLoadURLs: [URL] = []
var didRequestExpandURLs: [URL] = []
var didRequestExpandShouldToggleSidebar: [Bool] = []
var openSettingsCallCount = 0
var openSyncSettingsCallCount = 0

func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestToLoad url: URL) {
didRequestToLoadURLs.append(url)
}

func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL, shouldToggleSidebar: Bool) {
func aiChatContextualSheetCoordinator(_ coordinator: AIChatContextualSheetCoordinator, didRequestExpandWithURL url: URL) {
didRequestExpandURLs.append(url)
didRequestExpandShouldToggleSidebar.append(shouldToggleSidebar)
}

func aiChatContextualSheetCoordinatorDidRequestOpenSettings(_ coordinator: AIChatContextualSheetCoordinator) {
Expand Down Expand Up @@ -262,7 +260,7 @@ final class AIChatContextualSheetCoordinatorTests: XCTestCase {
let expandURL = URL(string: "https://duck.ai/chat/abc123")!

// When
sut.aiChatContextualSheetViewController(sut.sheetViewController!, didRequestExpandWithURL: expandURL, shouldToggleSidebar: false)
sut.aiChatContextualSheetViewController(sut.sheetViewController!, didRequestExpandWithURL: expandURL)

// Then
XCTAssertEqual(mockDelegate.didRequestExpandURLs, [expandURL])
Expand All @@ -276,7 +274,7 @@ final class AIChatContextualSheetCoordinatorTests: XCTestCase {
let expandURL = URL(string: "https://duck.ai/chat/abc123")!

// When
sut.aiChatContextualSheetViewController(sut.sheetViewController!, didRequestExpandWithURL: expandURL, shouldToggleSidebar: false)
sut.aiChatContextualSheetViewController(sut.sheetViewController!, didRequestExpandWithURL: expandURL)

// Then
XCTAssertNotNil(sut.sheetViewController)
Expand Down
Loading