Skip to content

Commit 2c757fe

Browse files
committed
修复群聊的工具相关
1 parent eec7c0e commit 2c757fe

3 files changed

Lines changed: 163 additions & 10 deletions

File tree

‎app/src/main/java/me/rerere/rikkahub/service/ChatService.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,7 @@ class ChatService(
19181918
searchMode = if (overrides.searchEnabled) overrides.searchMode else AssistantSearchMode.Off,
19191919
preferBuiltInSearch = overrides.searchEnabled && overrides.preferBuiltInSearch,
19201920
mcpServers = overrides.mcpServerIds,
1921-
localTools = emptyList(),
1921+
localTools = assistant.localTools,
19221922
enableMemory = overrides.memoryEnabled && assistant.enableMemory,
19231923
systemPrompt = updatedPrompt,
19241924
)

‎app/src/main/java/me/rerere/rikkahub/ui/components/ai/ChatInput.kt‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,20 +216,20 @@ enum class ChatInputUiMode {
216216
GroupChat,
217217
}
218218

219-
private data class GroupChatMentionKeySuggestion(
219+
internal data class GroupChatMentionKeySuggestion(
220220
val normalizedKey: String,
221221
val displayName: String,
222222
val seatCount: Int,
223223
val sampleAssistantId: Uuid?,
224224
)
225225

226-
private data class ActiveMentionContext(
226+
internal data class ActiveMentionContext(
227227
val atIndex: Int,
228228
val cursor: Int,
229229
val query: String,
230230
)
231231

232-
private fun findActiveMentionContext(
232+
internal fun findActiveMentionContext(
233233
text: String,
234234
cursor: Int,
235235
): ActiveMentionContext? {
@@ -250,7 +250,7 @@ private fun findActiveMentionContext(
250250
)
251251
}
252252

253-
private fun findMentionTokenStartForAtomicBackspace(
253+
internal fun findMentionTokenStartForAtomicBackspace(
254254
text: String,
255255
cursor: Int,
256256
validMentionKeys: Set<String>,
@@ -277,7 +277,7 @@ private fun findMentionTokenStartForAtomicBackspace(
277277
return atIndex
278278
}
279279

280-
private fun buildGroupChatMentionKeySuggestions(
280+
internal fun buildGroupChatMentionKeySuggestions(
281281
settings: Settings,
282282
template: GroupChatTemplate,
283283
defaultAssistantName: String,
@@ -321,7 +321,7 @@ private fun buildGroupChatMentionKeySuggestions(
321321
.sortedBy { it.displayName.lowercase(Locale.ROOT) }
322322
}
323323

324-
private fun filterGroupChatMentionSuggestions(
324+
internal fun filterGroupChatMentionSuggestions(
325325
suggestions: List<GroupChatMentionKeySuggestion>,
326326
queryNormalized: String,
327327
): List<GroupChatMentionKeySuggestion> {
@@ -1577,7 +1577,7 @@ private fun FilesPicker(
15771577
}
15781578
}
15791579

1580-
if (uiMode == ChatInputUiMode.Normal && !isKeyboardVisible) {
1580+
if (!isKeyboardVisible && (uiMode == ChatInputUiMode.Normal || conversation.messageNodes.isEmpty())) {
15811581
Spacer(modifier = Modifier.height(8.dp))
15821582

15831583
if (conversation.messageNodes.isEmpty()) {
@@ -1687,7 +1687,7 @@ private fun FilesPicker(
16871687
)
16881688
}
16891689

1690-
if (mcpServers.isNotEmpty()) {
1690+
if (uiMode == ChatInputUiMode.Normal && mcpServers.isNotEmpty()) {
16911691
val mcpInteractionSource = remember { MutableInteractionSource() }
16921692
val isMcpPressed by mcpInteractionSource.collectIsPressedAsState()
16931693
val mcpScale by animateFloatAsState(

‎app/src/main/java/me/rerere/rikkahub/ui/components/ai/MinimalChatInput.kt‎

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ import androidx.compose.material.icons.rounded.Summarize
6868
import androidx.compose.material.icons.rounded.Terminal
6969
import androidx.compose.material.icons.rounded.ViewModule
7070
import androidx.compose.material3.CircularProgressIndicator
71+
import androidx.compose.material3.Badge
72+
import androidx.compose.material3.DropdownMenu
73+
import androidx.compose.material3.DropdownMenuItem
7174
import androidx.compose.material3.ExperimentalMaterial3Api
7275
import androidx.compose.material3.HorizontalDivider
7376
import androidx.compose.material3.Icon
@@ -89,6 +92,7 @@ import androidx.compose.runtime.remember
8992
import androidx.compose.runtime.rememberCoroutineScope
9093
import androidx.compose.runtime.saveable.rememberSaveable
9194
import androidx.compose.runtime.setValue
95+
import androidx.compose.runtime.snapshotFlow
9296
import androidx.lifecycle.compose.collectAsStateWithLifecycle
9397
import kotlinx.coroutines.launch
9498
import androidx.compose.ui.Alignment
@@ -103,6 +107,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
103107
import androidx.compose.ui.res.stringResource
104108
import androidx.compose.ui.text.style.TextOverflow
105109
import androidx.compose.ui.unit.dp
110+
import androidx.compose.ui.window.PopupProperties
106111
import androidx.core.content.FileProvider
107112
import me.rerere.ai.provider.Model
108113
import me.rerere.rikkahub.R
@@ -112,6 +117,7 @@ import me.rerere.rikkahub.data.datastore.Settings
112117
import me.rerere.rikkahub.data.datastore.ConversationWorkDirBinding
113118
import me.rerere.rikkahub.data.datastore.ConversationWorkDirMode
114119
import me.rerere.rikkahub.data.datastore.findProvider
120+
import me.rerere.rikkahub.data.datastore.getAssistantById
115121
import me.rerere.rikkahub.data.datastore.getCurrentAssistant
116122
import me.rerere.rikkahub.data.datastore.getCurrentChatModel
117123
import me.rerere.rikkahub.data.datastore.getEffectiveWorkspaceRootTreeUri
@@ -120,6 +126,7 @@ import me.rerere.rikkahub.data.model.Assistant
120126
import me.rerere.rikkahub.data.model.Conversation
121127
import me.rerere.rikkahub.service.ChatService
122128
import me.rerere.rikkahub.ui.components.crop.CropImageScreen
129+
import me.rerere.rikkahub.ui.components.ui.UIAvatar
123130
import me.rerere.rikkahub.ui.components.ui.ToastType
124131
import me.rerere.rikkahub.ui.components.ui.permission.PermissionCamera
125132
import me.rerere.rikkahub.ui.components.ui.permission.PermissionManager
@@ -134,7 +141,9 @@ import me.rerere.rikkahub.utils.createChatFilesByContents
134141
import me.rerere.rikkahub.utils.getFileMimeType
135142
import me.rerere.rikkahub.utils.getFileNameFromUri
136143
import java.io.File
144+
import java.util.Locale
137145
import kotlinx.coroutines.Dispatchers
146+
import kotlinx.coroutines.flow.collect
138147
import kotlinx.coroutines.withContext
139148
import kotlin.uuid.Uuid
140149

@@ -172,6 +181,7 @@ fun MinimalChatInput(
172181
val toaster = LocalToaster.current
173182
val assistant = settings.getCurrentAssistant()
174183
val haptics = rememberPremiumHaptics(enabled = settings.displaySetting.enableUIHaptics)
184+
val defaultAssistantName = stringResource(R.string.assistant_page_default_assistant)
175185
val keyboardController = LocalSoftwareKeyboardController.current
176186
val localSettings = LocalSettings.current
177187

@@ -190,6 +200,30 @@ fun MinimalChatInput(
190200
var showPicker by remember { mutableStateOf(false) }
191201
var isFocused by remember { mutableStateOf(false) }
192202

203+
val groupChatTemplateForMentions = remember(
204+
uiMode,
205+
settings.groupChatTemplates,
206+
conversation.assistantId,
207+
) {
208+
if (uiMode != ChatInputUiMode.GroupChat) return@remember null
209+
settings.groupChatTemplates.firstOrNull { it.id == conversation.assistantId }
210+
}
211+
212+
val groupChatMentionKeys = remember(
213+
groupChatTemplateForMentions,
214+
settings.assistants,
215+
settings.assistantTags,
216+
defaultAssistantName,
217+
) {
218+
groupChatTemplateForMentions?.let { template ->
219+
buildGroupChatMentionKeySuggestions(
220+
settings = settings,
221+
template = template,
222+
defaultAssistantName = defaultAssistantName,
223+
)
224+
}.orEmpty()
225+
}
226+
193227
val workDirKey = conversation.id.toString()
194228
val currentWorkDirBinding = settings.conversationWorkDirs[workDirKey]
195229
val effectiveWorkspaceRootTreeUri = settings.getEffectiveWorkspaceRootTreeUri(conversation.id)
@@ -332,6 +366,125 @@ fun MinimalChatInput(
332366
bottom = 12.dp
333367
)
334368
)
369+
370+
val enableMentionSuggestions = groupChatMentionKeys.isNotEmpty()
371+
if (enableMentionSuggestions) {
372+
val validMentionKeySet = remember(groupChatMentionKeys) {
373+
groupChatMentionKeys.map { it.normalizedKey }.toSet()
374+
}
375+
val text = state.textContent.text.toString()
376+
val selection = state.textContent.selection
377+
val cursor = kotlin.math.max(selection.start, selection.end).coerceIn(0, text.length)
378+
379+
LaunchedEffect(enableMentionSuggestions, validMentionKeySet) {
380+
if (!enableMentionSuggestions) return@LaunchedEffect
381+
var previousText = state.textContent.text.toString()
382+
var previousSelection = state.textContent.selection
383+
var skipNext = false
384+
385+
snapshotFlow { state.textContent.text.toString() to state.textContent.selection }
386+
.collect { (currentText, currentSelection) ->
387+
if (skipNext) {
388+
skipNext = false
389+
previousText = currentText
390+
previousSelection = currentSelection
391+
return@collect
392+
}
393+
394+
val previousCursor =
395+
kotlin.math.max(previousSelection.start, previousSelection.end)
396+
.coerceIn(0, previousText.length)
397+
val currentCursor =
398+
kotlin.math.max(currentSelection.start, currentSelection.end)
399+
.coerceIn(0, currentText.length)
400+
401+
val isBackspace = previousSelection.start == previousSelection.end &&
402+
currentSelection.start == currentSelection.end &&
403+
previousText.length == currentText.length + 1 &&
404+
currentCursor == previousCursor - 1
405+
406+
if (isBackspace) {
407+
val mentionStart = findMentionTokenStartForAtomicBackspace(
408+
text = previousText,
409+
cursor = previousCursor,
410+
validMentionKeys = validMentionKeySet,
411+
)
412+
if (mentionStart != null && mentionStart < currentCursor) {
413+
skipNext = true
414+
haptics.perform(HapticPattern.Pop)
415+
state.replaceText(mentionStart, currentCursor, "")
416+
}
417+
}
418+
419+
previousText = currentText
420+
previousSelection = currentSelection
421+
}
422+
}
423+
424+
val activeMention = remember(text, cursor, selection) {
425+
if (selection.start != selection.end) return@remember null
426+
findActiveMentionContext(text = text, cursor = cursor)
427+
}
428+
429+
val queryNormalized = remember(activeMention) {
430+
activeMention?.query?.lowercase(Locale.ROOT).orEmpty()
431+
}
432+
433+
val filteredSuggestions = remember(groupChatMentionKeys, queryNormalized) {
434+
filterGroupChatMentionSuggestions(
435+
suggestions = groupChatMentionKeys,
436+
queryNormalized = queryNormalized,
437+
)
438+
}
439+
440+
var menuExpanded by remember { mutableStateOf(false) }
441+
LaunchedEffect(activeMention, isFocused, filteredSuggestions) {
442+
menuExpanded = isFocused && activeMention != null && filteredSuggestions.isNotEmpty()
443+
}
444+
445+
DropdownMenu(
446+
expanded = menuExpanded,
447+
onDismissRequest = { menuExpanded = false },
448+
modifier = Modifier.fillMaxWidth(),
449+
properties = PopupProperties(focusable = false),
450+
) {
451+
filteredSuggestions.take(8).forEach { suggestion ->
452+
val suggestionAssistant = suggestion.sampleAssistantId
453+
?.let(settings::getAssistantById)
454+
DropdownMenuItem(
455+
leadingIcon = {
456+
suggestionAssistant?.let { assistant ->
457+
UIAvatar(
458+
name = assistant.name.ifBlank { suggestion.displayName },
459+
value = assistant.avatar,
460+
modifier = Modifier.size(28.dp),
461+
loading = false,
462+
)
463+
}
464+
},
465+
text = { Text(text = "@${suggestion.displayName}") },
466+
trailingIcon = {
467+
if (suggestion.seatCount > 1) {
468+
Badge { Text(suggestion.seatCount.toString()) }
469+
}
470+
},
471+
onClick = {
472+
val context = activeMention ?: return@DropdownMenuItem
473+
val needsTrailingSpace =
474+
cursor >= text.length || !text[cursor].isWhitespace()
475+
val replacement = buildString {
476+
append('@')
477+
append(suggestion.displayName)
478+
if (needsTrailingSpace) append(' ')
479+
}
480+
haptics.perform(HapticPattern.Pop)
481+
state.replaceText(context.atIndex, context.cursor, replacement)
482+
menuExpanded = false
483+
},
484+
)
485+
}
486+
}
487+
}
335488

336489
// Action button - bottom-right, 4dp padding ("4dp all around")
337490
Box(
@@ -852,7 +1005,7 @@ private fun MinimalPickerContent(
8521005
}
8531006
)
8541007

855-
if (mcpServers.isNotEmpty()) {
1008+
if (uiMode == ChatInputUiMode.Normal && mcpServers.isNotEmpty()) {
8561009
MinimalPickerItem(
8571010
icon = {
8581011
if (mcpLoading) {

0 commit comments

Comments
 (0)