Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions PixelDefinitions/pixels/definitions/duck_chat.json5
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,17 @@
"type": "string",
"description": "Bucketed length of the submitted query",
"enum": ["short", "medium", "long", "very_long"]
},
{
"key": "default_mode",
"type": "string",
"description": "Which side the Search/Duck.ai toggle was showing by default.",
"enum": ["search", "duck_ai"]
},
{
"key": "toggle_visible",
"type": "boolean",
"description": "Whether the Search+Duck.ai toggle was shown."
}
]
},
Expand Down Expand Up @@ -1187,6 +1198,12 @@
"description": "Which tool was selected when the prompt was submitted",
"enum": ["none", "image_generation", "web_search"]
},
{
"key": "default_mode",
"type": "string",
"description": "Which side the Search/Duck.ai toggle was showing by default.",
"enum": ["search", "duck_ai"]
},
Comment thread
cursor[bot] marked this conversation as resolved.
{ "key": "model_id", "type": "string", "description": "Identifier of the model used for the prompt" },
{
"key": "reasoning_effort",
Expand All @@ -1213,6 +1230,12 @@
"description": "Which tool was selected when the prompt was submitted",
"enum": ["none", "image_generation", "web_search"]
},
{
"key": "default_mode",
"type": "string",
"description": "Which side the Search/Duck.ai toggle was showing by default.",
"enum": ["search", "duck_ai"]
},
{ "key": "model_id", "type": "string", "description": "Identifier of the model used for the prompt" },
{
"key": "reasoning_effort",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ interface DuckChatInternal : DuckChat {
*/
fun isContextualSheetRedesignEnabled(): Boolean

/**
* Returns the side the Search/Duck.ai toggle is defaulted to.
*/
fun resolvedTogglePosition(): NativeInputState.ToggleSelection

/**
* Checks whether DuckChat is enabled based on remote config flag.
*/
Expand Down Expand Up @@ -924,6 +929,19 @@ class RealDuckChat @Inject constructor(
duckChatFeatureRepository.setLastUsedTogglePosition(position)
}

override fun resolvedTogglePosition(): NativeInputState.ToggleSelection {
val defaultPosition = DefaultTogglePosition.fromName(duckChatFeatureRepository.observeDefaultTogglePosition().value)
val configured = if (defaultPosition == DefaultTogglePosition.LAST_USED) {
DefaultTogglePosition.fromName(duckChatFeatureRepository.observeLastUsedTogglePosition().value)
} else {
defaultPosition
}
return when (configured) {
DefaultTogglePosition.DUCK_AI -> NativeInputState.ToggleSelection.DUCK_AI
else -> NativeInputState.ToggleSelection.SEARCH
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

override fun observeLastUsedTogglePosition(): Flow<String?> =
duckChatFeatureRepository.observeLastUsedTogglePosition()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class MetricsNativeInputEventListener @Inject constructor(
}

override fun onSearchSubmitted(query: String) {
duckChatPixels.fireOmnibarQuerySubmitted(query)
sessionUsageMetric.onSearchSubmitted()
discoveryFunnel.onSearchSubmitted()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin
import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.nativeinput.NativeInputState
import com.duckduckgo.duckchat.api.nativeinput.NativeInputState.ToggleSelection
import com.duckduckgo.duckchat.impl.ModelTier
import com.duckduckgo.duckchat.impl.ReportMetric
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_CREATE_NEW_CHAT
Expand Down Expand Up @@ -196,6 +197,7 @@ interface DuckChatPixels {
hasFileAttachment: Boolean,
hasText: Boolean,
surface: DuckChatPixelSurface,
defaultMode: ToggleSelection?,
)

/** Prompt submitted while the unified input is in a Duck.ai chat context. Fires alongside [firePromptSubmitted]. */
Expand Down Expand Up @@ -242,7 +244,7 @@ interface DuckChatPixels {
fun fireCustomizeResponsesSelected(surface: DuckChatPixelSurface)
fun fireOmnibarShown()
fun fireOmnibarTextAreaFocused(landscape: Boolean)
fun fireOmnibarQuerySubmitted(query: String)
fun fireOmnibarQuerySubmitted(query: String, defaultMode: ToggleSelection?)
fun fireOmnibarModeSwitched(directionToSearch: Boolean, hadText: Boolean)
fun fireOmnibarClearButtonPressed(isSearchMode: Boolean)
fun fireOmnibarBackButtonPressed(isSearchMode: Boolean)
Expand Down Expand Up @@ -711,6 +713,7 @@ class RealDuckChatPixels @Inject constructor(
hasFileAttachment: Boolean,
hasText: Boolean,
surface: DuckChatPixelSurface,
defaultMode: ToggleSelection?,
) {
val params = buildMap {
put(DuckChatPixelParameters.SELECTED_TOOL, selectedTool)
Expand All @@ -720,6 +723,9 @@ class RealDuckChatPixels @Inject constructor(
put(DuckChatPixelParameters.HAS_FILE_ATTACHMENT, hasFileAttachment.toString())
put(DuckChatPixelParameters.HAS_TEXT, hasText.toString())
put(DuckChatPixelParameters.SURFACE, surface.value)
defaultMode
?.takeIf { surface == DuckChatPixelSurface.ADDRESS_BAR }
?.let { put(DuckChatPixelParameters.DEFAULT_MODE, it.pixelValue()) }
}
fireCountAndDaily(
DuckChatPixelName.DUCK_CHAT_UNIFIED_INPUT_PROMPT_SUBMITTED_COUNT,
Expand Down Expand Up @@ -960,11 +966,19 @@ class RealDuckChatPixels @Inject constructor(
}
}

override fun fireOmnibarQuerySubmitted(query: String) {
override fun fireOmnibarQuerySubmitted(
query: String,
defaultMode: ToggleSelection?,
) {
appCoroutineScope.launch(dispatcherProvider.io()) {
val params = buildMap {
put(DuckChatPixelParameters.TEXT_LENGTH_BUCKET, toQueryLengthBucket(query.length))
put(DuckChatPixelParameters.TOGGLE_VISIBLE, (defaultMode != null).toString())
defaultMode?.let { put(DuckChatPixelParameters.DEFAULT_MODE, it.pixelValue()) }
}
pixel.fire(
pixel = DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED,
parameters = mapOf(DuckChatPixelParameters.TEXT_LENGTH_BUCKET to toQueryLengthBucket(query.length)),
parameters = params,
)
pixel.fire(DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED_DAILY, type = Pixel.PixelType.Daily())
}
Expand Down Expand Up @@ -1330,6 +1344,8 @@ object DuckChatPixelParameters {
const val HAS_IMAGE_ATTACHMENT = "has_image_attachment"
const val HAS_FILE_ATTACHMENT = "has_file_attachment"
const val HAS_TEXT = "has_text"
const val DEFAULT_MODE = "default_mode"
const val TOGGLE_VISIBLE = "toggle_visible"
const val ATTACHMENT_SOURCE = "source"
const val FILE_VALIDATION_REASON = "reason"
const val SURFACE = "surface"
Expand Down Expand Up @@ -1592,3 +1608,8 @@ internal fun toQueryLengthBucket(length: Int): String =
length <= 100 -> "long"
else -> "very_long"
}

internal fun ToggleSelection.pixelValue(): String = when (this) {
ToggleSelection.SEARCH -> "search"
ToggleSelection.DUCK_AI -> "duck_ai"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.duckduckgo.duckchat.impl.store.DuckChatDataStore
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject

interface DuckChatFeatureRepository {
Expand Down Expand Up @@ -99,11 +100,11 @@ interface DuckChatFeatureRepository {

suspend fun getDefaultTogglePosition(): String?

fun observeDefaultTogglePosition(): Flow<String?>
fun observeDefaultTogglePosition(): StateFlow<String?>

suspend fun setLastUsedTogglePosition(position: String)

fun observeLastUsedTogglePosition(): Flow<String?>
fun observeLastUsedTogglePosition(): StateFlow<String?>
}

@SingleInstanceIn(AppScope::class)
Expand Down Expand Up @@ -215,13 +216,13 @@ class RealDuckChatFeatureRepository @Inject constructor(

override suspend fun getDefaultTogglePosition(): String? = duckChatDataStore.getDefaultTogglePosition()

override fun observeDefaultTogglePosition(): Flow<String?> = duckChatDataStore.observeDefaultTogglePosition()
override fun observeDefaultTogglePosition(): StateFlow<String?> = duckChatDataStore.observeDefaultTogglePosition()

override suspend fun setLastUsedTogglePosition(position: String) {
duckChatDataStore.setLastUsedTogglePosition(position)
}

override fun observeLastUsedTogglePosition(): Flow<String?> = duckChatDataStore.observeLastUsedTogglePosition()
override fun observeLastUsedTogglePosition(): StateFlow<String?> = duckChatDataStore.observeLastUsedTogglePosition()

private fun updateWidgets() {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ interface DuckChatDataStore {

suspend fun getDefaultTogglePosition(): String?

fun observeDefaultTogglePosition(): Flow<String?>
fun observeDefaultTogglePosition(): StateFlow<String?>

suspend fun setLastUsedTogglePosition(position: String)

fun observeLastUsedTogglePosition(): Flow<String?>
fun observeLastUsedTogglePosition(): StateFlow<String?>

suspend fun getSelectedModel(): SelectedModel?

Expand Down Expand Up @@ -453,13 +453,13 @@ class SharedPreferencesDuckChatDataStore @Inject constructor(
override suspend fun getDefaultTogglePosition(): String? =
store.data.firstOrNull()?.let { it[DUCK_AI_DEFAULT_TOGGLE_POSITION] }

override fun observeDefaultTogglePosition(): Flow<String?> = defaultTogglePositionFlow
override fun observeDefaultTogglePosition(): StateFlow<String?> = defaultTogglePositionFlow

override suspend fun setLastUsedTogglePosition(position: String) {
store.edit { prefs -> prefs[DUCK_AI_LAST_USED_TOGGLE_POSITION] = position }
}

override fun observeLastUsedTogglePosition(): Flow<String?> = lastUsedTogglePositionFlow
override fun observeLastUsedTogglePosition(): StateFlow<String?> = lastUsedTogglePositionFlow

override suspend fun getSelectedModel(): SelectedModel? {
val prefs = store.data.firstOrNull() ?: return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import com.duckduckgo.duckchat.impl.nativeinput.NativeInputPlugin
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelSurface
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixels
import com.duckduckgo.duckchat.impl.store.DefaultTogglePosition
import com.duckduckgo.duckchat.impl.ui.nativeinput.suggestions.ChatSuggestion
import com.duckduckgo.duckchat.impl.ui.nativeinput.suggestions.reader.ChatSuggestionsReader
import com.duckduckgo.duckchat.store.impl.DuckAiChat
Expand Down Expand Up @@ -237,13 +236,22 @@ class NativeInputModeWidgetViewModel @Inject constructor(
return nativeInputStateProvider.stateForTab(tabId).value.selectedTool
}

private fun currentInputState(): NativeInputState? =
activeTabId.value?.let { nativeInputStateProvider.stateForTab(it).value }

private fun resolvedTogglePositionIfVisible(state: NativeInputState? = currentInputState()): NativeInputState.ToggleSelection? =
if (state?.toggleVisible == true) duckChatInternal.resolvedTogglePosition() else null

/** The pixel surface for the active tab, derived from its published input context. */
private fun currentSurface(): DuckChatPixelSurface {
val inputContext = activeTabId.value?.let { nativeInputStateProvider.stateForTab(it).value.inputContext }
?: NativeInputState.InputContext.BROWSER
private fun currentSurface(state: NativeInputState? = currentInputState()): DuckChatPixelSurface {
val inputContext = state?.inputContext ?: NativeInputState.InputContext.BROWSER
return DuckChatPixelSurface.from(inputContext)
}

fun fireOmnibarQuerySubmitted(query: String) {
duckChatPixels.fireOmnibarQuerySubmitted(query, resolvedTogglePositionIfVisible(currentInputState()))
Comment thread
cursor[bot] marked this conversation as resolved.
}

/**
* Fires the unified prompt-submitted pixel plus, when a tool is selected, the matching per-tool
* submitted pixel. Called exactly once per Duck.ai (AI-chat) submission by the widget. Attachment
Expand All @@ -260,7 +268,8 @@ class NativeInputModeWidgetViewModel @Inject constructor(
Tool.WEB_SEARCH -> "web_search"
null -> "none"
}
val surface = currentSurface()
val inputState = currentInputState()
val surface = currentSurface(inputState)
duckChatPixels.firePromptSubmitted(
selectedTool = selectedToolParam,
modelId = getSelectedModelId(),
Expand All @@ -269,6 +278,7 @@ class NativeInputModeWidgetViewModel @Inject constructor(
hasFileAttachment = hasFileAttachment,
hasText = hasText,
surface = surface,
defaultMode = resolvedTogglePositionIfVisible(inputState),
)
when (tool) {
Tool.IMAGE_GENERATION -> duckChatPixels.fireImageGenerationSubmitted(surface)
Expand Down Expand Up @@ -417,9 +427,7 @@ class NativeInputModeWidgetViewModel @Inject constructor(

val chatSuggestionsUserEnabled: Flow<Boolean> = duckChatInternal.observeChatSuggestionsUserSettingEnabled()

val defaultTogglePosition: Flow<DefaultTogglePosition> = duckChatInternal.observeDefaultTogglePosition()

val lastUsedTogglePosition: Flow<String?> = duckChatInternal.observeLastUsedTogglePosition()
fun resolvedTogglePosition(): NativeInputState.ToggleSelection? = resolvedTogglePositionIfVisible()

suspend fun saveLastUsedTogglePosition(position: String) {
duckChatInternal.saveLastUsedTogglePosition(position)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ class NativeInputModeWidget @JvmOverloads constructor(
val textToSubmit = text.getTextToSubmit()?.toString()
if (textToSubmit != null) {
if (inputModeSwitch.selectedTabPosition == 0) {
viewModel.fireOmnibarQuerySubmitted(textToSubmit)
onSearchSent?.invoke(textToSubmit)
} else {
onChatSent?.invoke(textToSubmit)
Expand Down Expand Up @@ -1474,13 +1475,7 @@ class NativeInputModeWidget @JvmOverloads constructor(
// into the search-only experience.
val state = viewModel.state.firstOrNull() ?: return@launch
if (!state.toggleVisible) return@launch
val position = viewModel.defaultTogglePosition.firstOrNull() ?: return@launch
val resolved = if (position == DefaultTogglePosition.LAST_USED) {
DefaultTogglePosition.fromName(viewModel.lastUsedTogglePosition.firstOrNull())
} else {
Comment thread
cursor[bot] marked this conversation as resolved.
position
}
if (resolved == DefaultTogglePosition.DUCK_AI) {
if (viewModel.resolvedTogglePosition() == NativeInputState.ToggleSelection.DUCK_AI) {
selectChatTab()
}
}
Expand Down
Loading
Loading