Skip to content

Commit f282d41

Browse files
authored
Merge pull request #524 from feikukuai/main
feat: add Skill selector menu to chat input 输入框的加号菜单skills选项
2 parents fca30a2 + 146dcb2 commit f282d41

9 files changed

Lines changed: 208 additions & 7 deletions

File tree

app/src/main/java/com/ai/assistance/operit/ui/features/chat/components/AttachmentSelector.kt

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ import androidx.compose.material.icons.filled.Memory
4444
import androidx.compose.material.icons.filled.Notifications
4545
import androidx.compose.material.icons.filled.PhotoCamera
4646
import androidx.compose.material.icons.filled.ScreenshotMonitor
47+
import androidx.compose.material.icons.filled.AutoAwesome
48+
import androidx.compose.material3.AlertDialog
4749
import androidx.compose.material3.HorizontalDivider
4850
import androidx.compose.material3.Icon
4951
import androidx.compose.material3.MaterialTheme
5052
import androidx.compose.material3.Surface
5153
import androidx.compose.material3.Text
54+
import androidx.compose.material3.TextButton
5255
import androidx.compose.runtime.Composable
56+
import androidx.compose.runtime.LaunchedEffect
5357
import androidx.compose.runtime.getValue
5458
import androidx.compose.runtime.mutableStateOf
5559
import androidx.compose.runtime.remember
@@ -67,6 +71,8 @@ import androidx.compose.ui.unit.dp
6771
import androidx.compose.ui.window.Popup
6872
import androidx.compose.ui.window.PopupProperties
6973
import com.ai.assistance.operit.R
74+
import com.ai.assistance.operit.core.tools.skill.SkillPackage
75+
import com.ai.assistance.operit.data.skill.SkillRepository
7076
import kotlinx.coroutines.launch
7177
import java.io.File
7278
import androidx.core.content.ContextCompat
@@ -83,6 +89,7 @@ fun AttachmentSelectorPanel(
8389
onAttachNotifications: () -> Unit = {},
8490
onAttachLocation: () -> Unit = {},
8591
onAttachMemory: () -> Unit = {},
92+
onAttachSkill: (String) -> Unit = {},
8693
onTakePhoto: (Uri) -> Unit,
8794
userQuery: String = "",
8895
onDismiss: () -> Unit
@@ -94,6 +101,8 @@ fun AttachmentSelectorPanel(
94101
onDismiss = onDismiss,
95102
)
96103

104+
var showSkillDialog by remember { mutableStateOf(false) }
105+
97106
// 文件/图片选择器启动器
98107
val imagePickerLauncher = rememberLauncherForActivityResult(
99108
contract = ActivityResultContracts.GetMultipleContents()
@@ -212,15 +221,20 @@ fun AttachmentSelectorPanel(
212221
onAttachLocation()
213222
onDismiss()
214223
}
224+
),
225+
AttachmentPanelItem(
226+
icon = Icons.Default.AutoAwesome,
227+
label = context.getString(R.string.attachment_skill),
228+
onClick = { showSkillDialog = true }
215229
)
216230
)
217231

218-
val pages = panelItems.chunked(8).ifEmpty { listOf(emptyList()) }
232+
val pages = panelItems.chunked(9).ifEmpty { listOf(emptyList()) }
219233
val pagerState = rememberPagerState(pageCount = { pages.size })
220234

221235
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxWidth()) { pageIndex ->
222236
val pageItems = pages[pageIndex]
223-
val paddedItems = pageItems + List(8 - pageItems.size) { null }
237+
val paddedItems = pageItems + List(9 - pageItems.size) { null }
224238

225239
Column(modifier = Modifier.fillMaxWidth()) {
226240
// 第一行选项
@@ -231,7 +245,7 @@ fun AttachmentSelectorPanel(
231245
horizontalArrangement = Arrangement.SpaceEvenly,
232246
verticalAlignment = Alignment.CenterVertically
233247
) {
234-
paddedItems.take(4).forEach { item ->
248+
paddedItems.take(3).forEach { item ->
235249
if (item == null) {
236250
AttachmentOptionPlaceholder()
237251
} else {
@@ -254,7 +268,30 @@ fun AttachmentSelectorPanel(
254268
horizontalArrangement = Arrangement.SpaceEvenly,
255269
verticalAlignment = Alignment.CenterVertically
256270
) {
257-
paddedItems.drop(4).take(4).forEach { item ->
271+
paddedItems.drop(3).take(3).forEach { item ->
272+
if (item == null) {
273+
AttachmentOptionPlaceholder()
274+
} else {
275+
AttachmentOption(
276+
icon = item.icon,
277+
label = item.label,
278+
onClick = item.onClick
279+
)
280+
}
281+
}
282+
}
283+
284+
Spacer(modifier = Modifier.height(16.dp))
285+
286+
// 第三行选项
287+
Row(
288+
modifier =
289+
Modifier.fillMaxWidth().padding(horizontal = 16.dp)
290+
.heightIn(min = 96.dp),
291+
horizontalArrangement = Arrangement.SpaceEvenly,
292+
verticalAlignment = Alignment.CenterVertically
293+
) {
294+
paddedItems.drop(6).take(3).forEach { item ->
258295
if (item == null) {
259296
AttachmentOptionPlaceholder()
260297
} else {
@@ -298,9 +335,18 @@ fun AttachmentSelectorPanel(
298335
}
299336
}
300337
}
301-
}
302338

303-
/** Agent 模式用的附件弹窗(上方悬浮样式,功能与经典模式 8 项保持一致) */
339+
// 技能选择对话框
340+
SkillSelectorDialog(
341+
visible = showSkillDialog,
342+
onDismiss = { showSkillDialog = false },
343+
onSkillSelected = { skillName ->
344+
onAttachSkill(skillName)
345+
showSkillDialog = false
346+
onDismiss()
347+
}
348+
)
349+
}
304350
@Composable
305351
fun AttachmentSelectorPopupPanel(
306352
visible: Boolean,
@@ -311,6 +357,7 @@ fun AttachmentSelectorPopupPanel(
311357
onAttachNotifications: () -> Unit = {},
312358
onAttachLocation: () -> Unit = {},
313359
onAttachMemory: () -> Unit = {},
360+
onAttachSkill: (String) -> Unit = {},
314361
onTakePhoto: (Uri) -> Unit,
315362
onDismiss: () -> Unit
316363
) {
@@ -323,6 +370,8 @@ fun AttachmentSelectorPopupPanel(
323370
onDismiss = onDismiss,
324371
)
325372

373+
var showSkillDialog by remember { mutableStateOf(false) }
374+
326375
val imagePickerLauncher = rememberLauncherForActivityResult(
327376
contract = ActivityResultContracts.GetMultipleContents()
328377
) { uris: List<Uri> ->
@@ -406,6 +455,11 @@ fun AttachmentSelectorPopupPanel(
406455
onAttachLocation()
407456
onDismiss()
408457
}
458+
),
459+
AttachmentPanelItem(
460+
icon = Icons.Default.AutoAwesome,
461+
label = context.getString(R.string.attachment_skill),
462+
onClick = { showSkillDialog = true }
409463
)
410464
)
411465

@@ -476,6 +530,17 @@ fun AttachmentSelectorPopupPanel(
476530
}
477531
}
478532
}
533+
534+
// 技能选择对话框
535+
SkillSelectorDialog(
536+
visible = showSkillDialog,
537+
onDismiss = { showSkillDialog = false },
538+
onSkillSelected = { skillName ->
539+
onAttachSkill(skillName)
540+
showSkillDialog = false
541+
onDismiss()
542+
}
543+
)
479544
}
480545

481546
private data class AttachmentPanelItem(
@@ -601,3 +666,94 @@ private fun AttachmentOption(icon: ImageVector, label: String, onClick: () -> Un
601666
private fun AttachmentOptionPlaceholder() {
602667
Spacer(modifier = Modifier.width(70.dp).padding(horizontal = 8.dp, vertical = 8.dp))
603668
}
669+
670+
/** 技能选择对话框 */
671+
@Composable
672+
fun SkillSelectorDialog(
673+
visible: Boolean,
674+
onDismiss: () -> Unit,
675+
onSkillSelected: (String) -> Unit
676+
) {
677+
if (!visible) return
678+
679+
val context = LocalContext.current
680+
val skillRepository = remember { SkillRepository.getInstance(context) }
681+
var skills by remember { mutableStateOf<Map<String, SkillPackage>>(emptyMap()) }
682+
683+
LaunchedEffect(visible) {
684+
if (visible) {
685+
skills = skillRepository.getAiVisibleSkillPackages()
686+
}
687+
}
688+
689+
AlertDialog(
690+
onDismissRequest = onDismiss,
691+
title = {
692+
Text(
693+
text = context.getString(R.string.attachment_skill_select_title),
694+
style = MaterialTheme.typography.titleMedium
695+
)
696+
},
697+
text = {
698+
if (skills.isEmpty()) {
699+
Box(
700+
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
701+
contentAlignment = Alignment.Center
702+
) {
703+
Text(
704+
text = context.getString(R.string.attachment_skill_empty),
705+
style = MaterialTheme.typography.bodyMedium,
706+
color = MaterialTheme.colorScheme.onSurfaceVariant
707+
)
708+
}
709+
} else {
710+
Column(
711+
modifier = Modifier.fillMaxWidth().heightIn(max = 400.dp).verticalScroll(rememberScrollState())
712+
) {
713+
skills.forEach { (skillName, skillPackage) ->
714+
Surface(
715+
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
716+
shape = RoundedCornerShape(8.dp),
717+
color = Color.Transparent,
718+
onClick = { onSkillSelected(skillName) }
719+
) {
720+
Row(
721+
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 10.dp),
722+
verticalAlignment = Alignment.CenterVertically
723+
) {
724+
Icon(
725+
imageVector = Icons.Default.AutoAwesome,
726+
contentDescription = null,
727+
tint = MaterialTheme.colorScheme.primary,
728+
modifier = Modifier.size(20.dp)
729+
)
730+
Spacer(modifier = Modifier.width(12.dp))
731+
Column(modifier = Modifier.weight(1f)) {
732+
Text(
733+
text = skillName,
734+
style = MaterialTheme.typography.bodyMedium,
735+
color = MaterialTheme.colorScheme.onSurface
736+
)
737+
if (skillPackage.description.isNotBlank()) {
738+
Text(
739+
text = skillPackage.description,
740+
style = MaterialTheme.typography.bodySmall,
741+
color = MaterialTheme.colorScheme.onSurfaceVariant,
742+
maxLines = 2,
743+
overflow = TextOverflow.Ellipsis
744+
)
745+
}
746+
}
747+
}
748+
}
749+
}
750+
}
751+
}
752+
},
753+
confirmButton = {
754+
TextButton(onClick = onDismiss) {
755+
Text(context.getString(R.string.cancel))
756+
}
757+
}
758+
)
759+
}

app/src/main/java/com/ai/assistance/operit/ui/features/chat/components/style/input/agent/AgentChatInputSection.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ fun AgentChatInputSection(
176176
onAttachNotifications: () -> Unit = {},
177177
onAttachLocation: () -> Unit = {},
178178
onAttachMemory: () -> Unit = {},
179+
onAttachSkill: (String) -> Unit = {},
179180
onTakePhoto: (Uri) -> Unit,
180181
hasBackgroundImage: Boolean = false,
181182
chatInputTransparent: Boolean = false,
@@ -1443,6 +1444,7 @@ fun AgentChatInputSection(
14431444
onAttachNotifications = onAttachNotifications,
14441445
onAttachLocation = onAttachLocation,
14451446
onAttachMemory = onAttachMemory,
1447+
onAttachSkill = onAttachSkill,
14461448
onTakePhoto = onTakePhoto,
14471449
onDismiss = { setShowAttachmentPanel(false) },
14481450
)

app/src/main/java/com/ai/assistance/operit/ui/features/chat/components/style/input/classic/ClassicChatInputSection.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ fun ClassicChatInputSection(
9696
onAttachNotifications: () -> Unit = {},
9797
onAttachLocation: () -> Unit = {},
9898
onAttachMemory: () -> Unit = {},
99+
onAttachSkill: (String) -> Unit = {},
99100
onTakePhoto: (Uri) -> Unit,
100101
hasBackgroundImage: Boolean = false,
101102
chatInputTransparent: Boolean = false,
@@ -750,6 +751,7 @@ fun ClassicChatInputSection(
750751
onAttachNotifications = onAttachNotifications,
751752
onAttachLocation = onAttachLocation,
752753
onAttachMemory = onAttachMemory,
754+
onAttachSkill = onAttachSkill,
753755
onTakePhoto = onTakePhoto,
754756
userQuery = userMessage.text,
755757
onDismiss = { setShowAttachmentPanel(false) }

app/src/main/java/com/ai/assistance/operit/ui/features/chat/screens/AIChatScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,7 @@ private fun ChatInputBottomBar(
16471647
onAttachNotifications = actualViewModel::captureNotifications,
16481648
onAttachLocation = actualViewModel::captureLocation,
16491649
onAttachMemory = onShowMemoryFolderDialog,
1650+
onAttachSkill = actualViewModel::attachSkill,
16501651
onTakePhoto = actualViewModel::handleTakenPhoto,
16511652
hasBackgroundImage = hasBackgroundImage,
16521653
chatInputTransparent = chatInputTransparent,
@@ -1732,6 +1733,7 @@ private fun ChatInputBottomBar(
17321733
onAttachNotifications = actualViewModel::captureNotifications,
17331734
onAttachLocation = actualViewModel::captureLocation,
17341735
onAttachMemory = onShowMemoryFolderDialog,
1736+
onAttachSkill = actualViewModel::attachSkill,
17351737
onTakePhoto = actualViewModel::handleTakenPhoto,
17361738
hasBackgroundImage = hasBackgroundImage,
17371739
chatInputTransparent = chatInputTransparent,

app/src/main/java/com/ai/assistance/operit/ui/features/chat/viewmodel/ChatViewModel.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import com.ai.assistance.operit.services.ChatServiceCore
8989
import com.ai.assistance.operit.services.ChatServiceUiBridge
9090
import com.ai.assistance.operit.services.EmptyChatServiceUiBridge
9191
import com.ai.assistance.operit.ui.features.chat.util.MessageImageGenerator
92+
import com.ai.assistance.operit.core.tools.skill.SkillManager
9293
import com.ai.assistance.operit.ui.features.chat.components.CharacterSelectorTarget
9394
enum class ChatHistoryDisplayMode {
9495
BY_CHARACTER_CARD,
@@ -1680,6 +1681,32 @@ class ChatViewModel(private val context: Context) : ViewModel() {
16801681
}
16811682
}
16821683

1684+
/** Attaches a skill's content to the current message as an attachment */
1685+
fun attachSkill(skillName: String) {
1686+
viewModelScope.launch {
1687+
try {
1688+
val skillManager = SkillManager.getInstance(context)
1689+
val skillContent = skillManager.getSkillSystemPrompt(skillName)
1690+
if (skillContent != null) {
1691+
val attachment = AttachmentInfo(
1692+
filePath = "skill_${skillName}_${System.currentTimeMillis()}",
1693+
fileName = "技能: $skillName",
1694+
mimeType = "text/plain",
1695+
fileSize = skillContent.length.toLong(),
1696+
content = skillContent
1697+
)
1698+
attachmentDelegate.addAttachments(listOf(attachment))
1699+
uiStateDelegate.showToast(context.getString(R.string.attachment_skill_added, skillName))
1700+
} else {
1701+
uiStateDelegate.showToast(context.getString(R.string.attachment_skill_failed, skillName))
1702+
}
1703+
} catch (e: Exception) {
1704+
AppLogger.e(TAG, "添加技能附件失败", e)
1705+
uiStateDelegate.showToast(context.getString(R.string.attachment_skill_failed, skillName))
1706+
}
1707+
}
1708+
}
1709+
16831710
private suspend fun awaitCurrentChat(chatId: String, maxWaitCount: Int = 40): Boolean {
16841711
var waitCount = 0
16851712
while (currentChatId.value != chatId && waitCount < maxWaitCount) {

app/src/main/java/com/ai/assistance/operit/ui/features/chat/webview/computer/ComputerScreen.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ fun ComputerScreen() {
4545
TerminalScreen(
4646
env = terminalEnv,
4747
useLocalImeHandling = false,
48-
checkUpdatesOnEnter = false,
4948
)
5049
}
5150
}

app/src/main/java/com/ai/assistance/operit/ui/floating/ui/window/components/AttachmentPanel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Memory
2626
import androidx.compose.material.icons.filled.Notifications
2727
import androidx.compose.material.icons.filled.Crop
2828
import androidx.compose.material.icons.filled.ScreenshotMonitor
29+
import androidx.compose.material.icons.filled.AutoAwesome
2930
import androidx.compose.material3.HorizontalDivider
3031
import androidx.compose.material3.Icon
3132
import androidx.compose.material3.MaterialTheme
@@ -55,6 +56,7 @@ fun FloatingAttachmentPanel(
5556
onAttachNotifications: () -> Unit,
5657
onAttachLocation: () -> Unit,
5758
onAttachScreenOcr: () -> Unit,
59+
onAttachSkill: (String) -> Unit = {},
5860
onDismiss: () -> Unit
5961
) {
6062
// 定义附件选项列表,便于使用LazyRow
@@ -78,6 +80,11 @@ fun FloatingAttachmentPanel(
7880
icon = Icons.Default.Crop,
7981
label = stringResource(R.string.screen_ocr_select),
8082
onClick = onAttachScreenOcr
83+
),
84+
AttachmentOptionData(
85+
icon = Icons.Default.AutoAwesome,
86+
label = stringResource(R.string.attachment_skill),
87+
onClick = { onAttachSkill("") }
8188
)
8289
)
8390

0 commit comments

Comments
 (0)