Skip to content

Commit 7159e2d

Browse files
Per-chat reasoning effort in contextual sheet
1 parent 6f9a5f1 commit 7159e2d

5 files changed

Lines changed: 88 additions & 9 deletions

File tree

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/ContextualNativeInputManager.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.duckduckgo.js.messaging.api.JsMessaging
3030
import com.duckduckgo.js.messaging.api.SubscriptionEventData
3131
import com.google.android.material.card.MaterialCardView
3232
import com.squareup.anvil.annotations.ContributesBinding
33+
import kotlinx.coroutines.flow.Flow
3334
import kotlinx.coroutines.flow.launchIn
3435
import kotlinx.coroutines.flow.onEach
3536
import org.json.JSONArray
@@ -43,6 +44,7 @@ interface ContextualNativeInputManager {
4344
widget: NativeInputModeWidget,
4445
jsMessaging: JsMessaging,
4546
lifecycleOwner: LifecycleOwner,
47+
chatIdFlow: Flow<String?>,
4648
onSearchSubmitted: (String) -> Unit,
4749
onCameraCaptureRequested: (ValueCallback<Array<Uri>>) -> Unit = {},
4850
onFilePickerRequested: (ValueCallback<Array<Uri>>, List<String>) -> Unit = { _, _ -> },
@@ -68,6 +70,7 @@ class RealContextualNativeInputManager @Inject constructor(
6870
widget: NativeInputModeWidget,
6971
jsMessaging: JsMessaging,
7072
lifecycleOwner: LifecycleOwner,
73+
chatIdFlow: Flow<String?>,
7174
onSearchSubmitted: (String) -> Unit,
7275
onCameraCaptureRequested: (ValueCallback<Array<Uri>>) -> Unit,
7376
onFilePickerRequested: (ValueCallback<Array<Uri>>, List<String>) -> Unit,
@@ -77,7 +80,7 @@ class RealContextualNativeInputManager @Inject constructor(
7780
this.widget = widget
7881

7982
applyCardShape(card)
80-
setupWidget(tabId, widget, onSearchSubmitted, onCameraCaptureRequested, onFilePickerRequested)
83+
setupWidget(tabId, widget, chatIdFlow, onSearchSubmitted, onCameraCaptureRequested, onFilePickerRequested)
8184
observeNativeInputSetting(lifecycleOwner)
8285
}
8386

@@ -113,11 +116,13 @@ class RealContextualNativeInputManager @Inject constructor(
113116
private fun setupWidget(
114117
tabId: String,
115118
widget: NativeInputModeWidget,
119+
chatIdFlow: Flow<String?>,
116120
onSearchSubmitted: (String) -> Unit,
117121
onCameraCaptureRequested: (ValueCallback<Array<Uri>>) -> Unit,
118122
onFilePickerRequested: (ValueCallback<Array<Uri>>, List<String>) -> Unit,
119123
) {
120124
widget.configureContextual(tabId)
125+
widget.bindChatIdSource(chatIdFlow)
121126
widget.hideMainButtons()
122127
widget.onStopTapped = ::sendStopEvent
123128
widget.bindAttachmentCallbacks(

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualFragment.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ class DuckChatContextualFragment :
433433
widget = binding.contextualNativeInputWidget,
434434
jsMessaging = contentScopeScripts,
435435
lifecycleOwner = viewLifecycleOwner,
436+
chatIdFlow = viewModel.chatId,
436437
onSearchSubmitted = { query ->
437438
viewModel.onContextualClose()
438439
startActivity(browserNav.openInNewTab(requireContext(), query))

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModel.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ class DuckChatContextualViewModel @Inject constructor(
8080
var updatedPageContext: String = ""
8181
var sheetTabId: String = ""
8282

83+
// Chat id currently shown in the contextual webview, derived from the URL query param.
84+
// Null when in INPUT mode (composing a new chat) or when the URL has no chatID yet.
85+
private val _chatId = MutableStateFlow<String?>(null)
86+
val chatId: StateFlow<String?> = _chatId.asStateFlow()
87+
8388
@VisibleForTesting
8489
internal var isPageContextRequested: Boolean = false
8590

@@ -182,6 +187,8 @@ class DuckChatContextualViewModel @Inject constructor(
182187
}
183188
} else {
184189
withContext(dispatchers.main()) {
190+
fullModeUrl = existingChatUrl
191+
_chatId.value = extractChatId(existingChatUrl)
185192
_viewState.update { state ->
186193
state.copy(
187194
sheetMode = SheetMode.WEBVIEW,
@@ -233,6 +240,8 @@ class DuckChatContextualViewModel @Inject constructor(
233240
val hasChatHistory = hasChatId(existingChatUrl)
234241

235242
withContext(dispatchers.main()) {
243+
fullModeUrl = existingChatUrl
244+
_chatId.value = extractChatId(existingChatUrl)
236245
_viewState.update { current ->
237246
current.copy(
238247
sheetMode = SheetMode.WEBVIEW,
@@ -280,6 +289,7 @@ class DuckChatContextualViewModel @Inject constructor(
280289
fullModeUrl = url
281290
val sheetMode = _viewState.value.sheetMode
282291
if (sheetMode == SheetMode.WEBVIEW) {
292+
_chatId.value = extractChatId(url)
283293
val hasChatId = hasChatId(url)
284294

285295
viewModelScope.launch {
@@ -636,6 +646,8 @@ class DuckChatContextualViewModel @Inject constructor(
636646
contextualDataStore.clearTabChatUrl(currentTabId)
637647

638648
withContext(dispatchers.main()) {
649+
fullModeUrl = ""
650+
_chatId.value = null
639651
_viewState.update {
640652
it.copy(
641653
sheetMode = SheetMode.INPUT,
@@ -655,11 +667,10 @@ class DuckChatContextualViewModel @Inject constructor(
655667
}
656668
}
657669

658-
private fun hasChatId(url: String?): Boolean {
659-
return url?.toUri()?.getQueryParameter(CHAT_ID_PARAM)
660-
.orEmpty()
661-
.isNotBlank()
662-
}
670+
private fun hasChatId(url: String?): Boolean = !extractChatId(url).isNullOrBlank()
671+
672+
private fun extractChatId(url: String?): String? =
673+
url?.toUri()?.getQueryParameter(CHAT_ID_PARAM)?.takeIf { it.isNotBlank() }
663674

664675
private suspend fun shouldReuseStoredChatUrl(tabId: String): Boolean {
665676
val lastClosedTimestamp = contextualDataStore.getTabClosedTimestamp(tabId) ?: return true

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/nativeinput/views/ReasoningModePickerViewModel.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,10 @@ class ReasoningModePickerViewModel @Inject constructor(
121121
fun onModeTapped(mode: ReasoningMode, surface: PickerSurface) {
122122
// Drop taps that race past the picker hiding (loading, mismatch, no accessible rows).
123123
if (!state.value.visible) return
124+
val modelState = modelManager.modelState.value
124125
val chat = currentChat.value
125-
val chatResolution = chat?.let { ReasoningResolver.forChat(it, modelManager.modelState.value) }
126-
val available = chatResolution?.available ?: modelManager.modelState.value.availableReasoningModes
126+
val chatResolution = chat?.let { ReasoningResolver.forChat(it, modelState) }
127+
val available = chatResolution?.available ?: modelState.availableReasoningModes
127128

128129
val match = available.firstOrNull { it.mode == mode }
129130
if (match == null) {
@@ -138,7 +139,7 @@ class ReasoningModePickerViewModel @Inject constructor(
138139
}
139140
return
140141
}
141-
val userTier = modelManager.modelState.value.userTier
142+
val userTier = modelState.userTier
142143
val requiredTier = match.access?.requiredTier ?: run {
143144
logcat { "Duck.ai reasoning picker: gated mode $mode has no public required tier, ignoring." }
144145
return

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModelTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,67 @@ class DuckChatContextualViewModelTest {
12011201
assertNull(contextualDataStore.getTabChatUrl(tabId))
12021202
}
12031203

1204+
@Test
1205+
fun `chatId starts null`() {
1206+
assertNull(testee.chatId.value)
1207+
}
1208+
1209+
@Test
1210+
fun `when sheet opened with stored chat url then chatId set before page load`() = runTest {
1211+
val tabId = "tab-1"
1212+
val storedUrl = "https://duck.ai/chat?chatID=abc-123"
1213+
contextualDataStore.persistTabChatUrl(tabId, storedUrl)
1214+
1215+
testee.onSheetOpened(tabId)
1216+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1217+
1218+
assertEquals("abc-123", testee.chatId.value)
1219+
}
1220+
1221+
@Test
1222+
fun `onChatPageLoaded in WEBVIEW mode then chatId set from url`() = runTest {
1223+
val tabId = "tab-1"
1224+
val storedUrl = "https://duck.ai/chat?chatID=abc-123"
1225+
contextualDataStore.persistTabChatUrl(tabId, storedUrl)
1226+
testee.onSheetOpened(tabId)
1227+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1228+
1229+
val newUrl = "https://duck.ai/chat?chatID=xyz-789"
1230+
testee.onChatPageLoaded(newUrl)
1231+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1232+
1233+
assertEquals("xyz-789", testee.chatId.value)
1234+
}
1235+
1236+
@Test
1237+
fun `onChatPageLoaded in INPUT mode then chatId not set`() = runTest {
1238+
val tabId = "tab-1"
1239+
testee.onSheetOpened(tabId)
1240+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1241+
assertNull(testee.chatId.value)
1242+
1243+
val staleUrl = "https://duck.ai/chat?chatID=stale-123"
1244+
testee.onChatPageLoaded(staleUrl)
1245+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1246+
1247+
assertNull(testee.chatId.value)
1248+
}
1249+
1250+
@Test
1251+
fun `onNewChatRequested clears chatId`() = runTest {
1252+
val tabId = "tab-1"
1253+
val storedUrl = "https://duck.ai/chat?chatID=abc-123"
1254+
contextualDataStore.persistTabChatUrl(tabId, storedUrl)
1255+
testee.onSheetOpened(tabId)
1256+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1257+
assertEquals("abc-123", testee.chatId.value)
1258+
1259+
testee.onNewChatRequested()
1260+
coroutineRule.testDispatcher.scheduler.advanceUntilIdle()
1261+
1262+
assertNull(testee.chatId.value)
1263+
}
1264+
12041265
@Test
12051266
fun `onNewChatRequested clears stored url for current tab`() = runTest {
12061267
val tabId = "tab-1"

0 commit comments

Comments
 (0)