新增: AI Chat @灵感提及(ChatInput mention idea组)

- ChatInput.vue: MentionItem type加idea + mentionItems加idea过滤(title/description projectsStore.ideas) + mentionGroups加idea组 + selectMention插入[灵感: title]
- i18n zh/en: mentionGroupIdea
灵感走store同步computed(同project/task模式 无IPC)
This commit is contained in:
2026-06-19 16:26:42 +08:00
parent c38cefe47b
commit ec1daa8844
3 changed files with 24 additions and 5 deletions

View File

@@ -119,7 +119,7 @@ import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai' import { useAiStore } from '../../stores/ai'
import { useProjectStore } from '../../stores/project' import { useProjectStore } from '../../stores/project'
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord } from '../../api/types' import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord } from '../../api/types'
const props = defineProps<{ const props = defineProps<{
/** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */ /** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
@@ -324,7 +324,7 @@ function clearSkill() {
// 触发:输入框光标前最近的 @ 后无空白/换行 → 激活;Esc/光标移开/选中插入 → 关闭。 // 触发:输入框光标前最近的 @ 后无空白/换行 → 激活;Esc/光标移开/选中插入 → 关闭。
// Enter 不冲突:popover 开时 Enter 插入选中(覆盖发送),关时 Enter 发送(原逻辑)。 // Enter 不冲突:popover 开时 Enter 插入选中(覆盖发送),关时 Enter 发送(原逻辑)。
interface MentionItem { interface MentionItem {
type: 'project' | 'task' type: 'project' | 'task' | 'idea'
/** 扁平后唯一 key(用于 key/导航扁平索引定位) */ /** 扁平后唯一 key(用于 key/导航扁平索引定位) */
uid: string uid: string
/** 实体原始 id(供后端解析,目前仅插入 [类型:名] 文本,id 未透传) */ /** 实体原始 id(供后端解析,目前仅插入 [类型:名] 文本,id 未透传) */
@@ -342,6 +342,7 @@ const mentionStart = ref(-1)
const mentionGroupLabel = computed(() => ({ const mentionGroupLabel = computed(() => ({
project: t('aiChat.mentionGroupProject'), project: t('aiChat.mentionGroupProject'),
task: t('aiChat.mentionGroupTask'), task: t('aiChat.mentionGroupTask'),
idea: t('aiChat.mentionGroupIdea'),
})) }))
/** 当前 @ 查询文本(@ 之后的串,不含 @) */ /** 当前 @ 查询文本(@ 之后的串,不含 @) */
@@ -378,19 +379,33 @@ const mentionItems = computed<MentionItem[]>(() => {
desc: tk.description || '', desc: tk.description || '',
typeLabel: t('aiChat.mentionGroupTask'), typeLabel: t('aiChat.mentionGroupTask'),
})) }))
return [...projectItems, ...taskItems] const ideas = (projectsStore.ideas || []) as IdeaRecord[]
const ideaItems: MentionItem[] = ideas
.filter(it => match(it.title) || match(it.description || ''))
.slice(0, 20)
.map(it => ({
type: 'idea' as const,
uid: 'i-' + it.id,
id: it.id,
name: it.title,
desc: it.description || '',
typeLabel: t('aiChat.mentionGroupIdea'),
}))
return [...projectItems, ...taskItems, ...ideaItems]
}) })
/** 联想候选总数(未过滤,用于区分"无匹配" vs "空") */ /** 联想候选总数(未过滤,用于区分"无匹配" vs "空") */
const mentionTotal = computed(() => const mentionTotal = computed(() =>
((projectsStore.projects || []) as ProjectRecord[]).length ((projectsStore.projects || []) as ProjectRecord[]).length
+ ((projectsStore.tasks || []) as TaskRecord[]).length, + ((projectsStore.tasks || []) as TaskRecord[]).length
+ ((projectsStore.ideas || []) as IdeaRecord[]).length,
) )
/** 按类型分组(模板按组渲染组标题) */ /** 按类型分组(模板按组渲染组标题) */
const mentionGroups = computed(() => [ const mentionGroups = computed(() => [
{ key: 'project', label: mentionGroupLabel.value.project, items: mentionItems.value.filter(i => i.type === 'project') }, { key: 'project', label: mentionGroupLabel.value.project, items: mentionItems.value.filter(i => i.type === 'project') },
{ key: 'task', label: mentionGroupLabel.value.task, items: mentionItems.value.filter(i => i.type === 'task') }, { key: 'task', label: mentionGroupLabel.value.task, items: mentionItems.value.filter(i => i.type === 'task') },
{ key: 'idea', label: mentionGroupLabel.value.idea, items: mentionItems.value.filter(i => i.type === 'idea') },
]) ])
/** 扁平下标(组内 item 在扁平列表中的位置,供键盘导航 ↑↓/高亮) */ /** 扁平下标(组内 item 在扁平列表中的位置,供键盘导航 ↑↓/高亮) */
@@ -405,7 +420,9 @@ function selectMention(item: MentionItem) {
const after = inputText.value.slice(cursorAfter) const after = inputText.value.slice(cursorAfter)
const label = item.type === 'project' const label = item.type === 'project'
? `[${t('aiChat.mentionGroupProject')}: ${item.name}]` ? `[${t('aiChat.mentionGroupProject')}: ${item.name}]`
: `[${t('aiChat.mentionGroupTask')}: ${item.name}]` : item.type === 'task'
? `[${t('aiChat.mentionGroupTask')}: ${item.name}]`
: `[${t('aiChat.mentionGroupIdea')}: ${item.name}]`
inputText.value = before + label + ' ' + after inputText.value = before + label + ' ' + after
mentionOpen.value = false mentionOpen.value = false
mentionStart.value = -1 mentionStart.value = -1

View File

@@ -90,6 +90,7 @@ export default {
mentionHint: "{'@'} to mention project/task", mentionHint: "{'@'} to mention project/task",
mentionGroupProject: 'Projects', mentionGroupProject: 'Projects',
mentionGroupTask: 'Tasks', mentionGroupTask: 'Tasks',
mentionGroupIdea: 'Ideas',
mentionNoMatch: 'No matching entity', mentionNoMatch: 'No matching entity',
mentionEmpty: 'No project/task to mention', mentionEmpty: 'No project/task to mention',

View File

@@ -90,6 +90,7 @@ export default {
mentionHint: "{'@'} 引用项目/任务", mentionHint: "{'@'} 引用项目/任务",
mentionGroupProject: '项目', mentionGroupProject: '项目',
mentionGroupTask: '任务', mentionGroupTask: '任务',
mentionGroupIdea: '灵感',
mentionNoMatch: '无匹配实体', mentionNoMatch: '无匹配实体',
mentionEmpty: '暂无可引用的项目/任务', mentionEmpty: '暂无可引用的项目/任务',