feat: 为ChatUI添加指令候选功能#8279
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
handleInputandfilteredCommands, consider usinglocalPromptinstead ofprops.promptso the suggestion logic always runs against the latest textarea value rather than relying on the parent to have already propagated the prop update. - In
handleBlur, thesetTimeoutclosing the suggestion panel isn’t stored or cleared; tracking the timeout ID and cancelling it when focus returns quickly (or on unmount) would avoid potential delayed hiding after the user refocuses.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `handleInput` and `filteredCommands`, consider using `localPrompt` instead of `props.prompt` so the suggestion logic always runs against the latest textarea value rather than relying on the parent to have already propagated the prop update.
- In `handleBlur`, the `setTimeout` closing the suggestion panel isn’t stored or cleared; tracking the timeout ID and cancelling it when focus returns quickly (or on unmount) would avoid potential delayed hiding after the user refocuses.
## Individual Comments
### Comment 1
<location path="dashboard/src/components/chat/ChatInput.vue" line_range="117" />
<code_context>
+ :selected-index="selectedCommandIndex"
+ :is-dark="isDark"
+ @select="handleCommandSelect"
+ @update-selected-index="selectedCommandIndex = $event"
+ />
<textarea
</code_context>
<issue_to_address>
**issue (bug_risk):** The emitted event name from `CommandSuggestion` likely won't match this listener, so the selected index may never update.
In `CommandSuggestion.vue`, `defineEmits` uses `updateSelectedIndex` (camelCase), but this parent listens for `@update-selected-index` (kebab-case). In Vue 3, custom events are case-sensitive and not normalized, so emitting `updateSelectedIndex` will not trigger `@update-selected-index`, and the index won't update. Please either:
- Switch the emit to kebab-case (`emit('update-selected-index', index)` and `defineEmits<{ 'update-selected-index': [index: number] }>()`), or
- Change the listener to camelCase (`@updateSelectedIndex="selectedCommandIndex = $event"`).
Kebab-case in templates is generally preferred.
</issue_to_address>
### Comment 2
<location path="dashboard/src/components/chat/CommandSuggestion.vue" line_range="2-10" />
<code_context>
+<template>
+ <div
+ v-if="visible && filteredCommands.length > 0"
+ class="command-suggestion-panel"
+ :class="{ 'is-dark': isDark }"
+ :style="panelStyle"
+ >
+ <div class="command-suggestion-list">
+ <div
+ v-for="(cmd, index) in filteredCommands"
+ :key="cmd.handler_full_name"
+ class="command-suggestion-item"
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `handler_full_name` as the v-for key can cause duplicate keys when a command has aliases.
Because the list expands each base command into all of its aliases, multiple entries can share the same `handler_full_name`, which will trigger Vue duplicate-key warnings and may cause unstable rendering. Use a composite key that’s unique per suggestion, for example:
```vue
:key="cmd.handler_full_name + '::' + cmd.effective_command"
```
or another reliably unique combination for each item.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| :selected-index="selectedCommandIndex" | ||
| :is-dark="isDark" | ||
| @select="handleCommandSelect" | ||
| @update-selected-index="selectedCommandIndex = $event" |
There was a problem hiding this comment.
issue (bug_risk): The emitted event name from CommandSuggestion likely won't match this listener, so the selected index may never update.
In CommandSuggestion.vue, defineEmits uses updateSelectedIndex (camelCase), but this parent listens for @update-selected-index (kebab-case). In Vue 3, custom events are case-sensitive and not normalized, so emitting updateSelectedIndex will not trigger @update-selected-index, and the index won't update. Please either:
- Switch the emit to kebab-case (
emit('update-selected-index', index)anddefineEmits<{ 'update-selected-index': [index: number] }>()), or - Change the listener to camelCase (
@updateSelectedIndex="selectedCommandIndex = $event").
Kebab-case in templates is generally preferred.
| <div | ||
| v-if="visible && filteredCommands.length > 0" | ||
| class="command-suggestion-panel" | ||
| :class="{ 'is-dark': isDark }" | ||
| :style="panelStyle" | ||
| > | ||
| <div class="command-suggestion-list"> | ||
| <div | ||
| v-for="(cmd, index) in filteredCommands" |
There was a problem hiding this comment.
issue (bug_risk): Using handler_full_name as the v-for key can cause duplicate keys when a command has aliases.
Because the list expands each base command into all of its aliases, multiple entries can share the same handler_full_name, which will trigger Vue duplicate-key warnings and may cause unstable rendering. Use a composite key that’s unique per suggestion, for example:
:key="cmd.handler_full_name + '::' + cmd.effective_command"or another reliably unique combination for each item.
There was a problem hiding this comment.
Code Review
This pull request implements a command suggestion feature for the chat interface, enabling users to discover and select commands via a new suggestion panel. The implementation includes fetching commands from the backend, filtering them based on user input, and providing keyboard navigation. Review feedback highlights a bug regarding duplicate keys in the suggestion list when aliases are present, identifies redundant logic and dead code for removal, and suggests a more robust approach for handling input focus transitions compared to the current timeout-based implementation.
| <div class="command-suggestion-list"> | ||
| <div | ||
| v-for="(cmd, index) in filteredCommands" | ||
| :key="cmd.handler_full_name" |
|
|
||
| const { tm } = useModuleI18n("features/chat"); | ||
|
|
||
| const filteredCommands = computed(() => props.commands); |
| caretPosition?: { left: number; top: number } | null; | ||
| } | ||
|
|
||
| const props = withDefaults(defineProps<Props>(), { | ||
| caretPosition: null, |
| const hasMatch = enabledCommands.value.some((cmd) => | ||
| cmd.effective_command.toLowerCase().startsWith(prefix), | ||
| ); |
| setTimeout(() => { | ||
| showCommandSuggestion.value = false; | ||
| }, 200); |
Modifications / 改动点
resolve #8277
一、需求背景
在 AstrBot Dashboard 的 ChatUI 聊天界面中,当用户在输入框输入以
/开头的命令时,希望能够自动弹出候选指令列表,帮助用户快速补全命令。该功能需要:二、设计方案
2.1 基本设计
在
ChatInput.vue内部集成命令提示逻辑:axios.get('/api/commands')获取并缓存指令列表/开头时触发匹配优点:改动范围最小,无需修改父组件,减少跨组件耦合
缺点:每个 ChatInput 实例独立请求 API(但实际场景中通常只有一个实例)
2.2 组件架构
2.3 数据流
三、实现详情
3.1 新增文件
dashboard/src/components/chat/CommandSuggestion.vue命令提示浮动面板子组件,负责:
commands(候选列表)、selectedIndex(当前选中项)、isDark(暗色模式)关键 Props:
关键 Events:
3.2 修改文件
dashboard/src/components/chat/ChatInput.vue新增导入:
新增状态:
核心计算属性 - enabledCommands:
从所有指令中展平获取启用的普通指令和子指令,统一添加
/前缀:过滤计算属性 - filteredCommands:
输入监听 - handleInput:
键盘事件处理 - handleKeyDown(命令提示激活时拦截):
命令选择 - handleCommandSelect:
API 请求 - fetchCommands:
组件挂载时预加载:
模板集成:
3.3 国际化翻译
dashboard/src/i18n/locales/zh-CN/features/chat.json{ "commandSuggestion": { "navigate": "选择", "select": "确认", "close": "关闭" } }dashboard/src/i18n/locales/en-US/features/chat.json{ "commandSuggestion": { "navigate": "Navigate", "select": "Select", "close": "Close" } }dashboard/src/i18n/locales/ru-RU/features/chat.json{ "commandSuggestion": { "navigate": "Выбор", "select": "Подтвердить", "close": "Закрыть" } }四、关键设计决策
4.1 子命令前缀处理
AstrBot 中子命令的
effective_command格式为"music play"(不带/),而普通命令为"/reset"(带/)。处理方式:在
enabledCommands计算属性中统一为所有命令添加/前缀,确保用户输入/时能匹配到所有类型的命令。4.2 别名处理
别名需要结合
parent_signature拼接完整命令名:4.3 指令组过滤
指令组(
type === "group")本身不可执行,因此不加入候选列表,仅递归处理其子命令。4.4 去重机制
使用
Set<string>对effective_command进行去重,避免同一命令因别名或重复注册而多次出现。4.5 Blur 延迟关闭
为避免点击候选项时面板已消失,
handleBlur使用 200ms 延迟关闭:4.6 错误处理
API 请求失败时静默处理,不影响聊天核心功能:
五、功能特性清单
/前缀触发命令提示enabled=true)的指令Screenshots or Test Results / 运行截图或测试结果
支持内置和插件指令提示

支持二级命令提示

支持暗色模式

Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Add slash-command suggestion support to the chat input UI, including a floating suggestion panel backed by a preloaded commands list and integrated keyboard navigation.
New Features: