ForcePilot/web/src/utils/mention_utils.js

185 lines
5.8 KiB
JavaScript
Raw Normal View History

import { getDisplayFileName } from '@/utils/file_utils'
export const mentionTypePrefixMap = {
file: 'file',
knowledge: 'knowledge',
mcp: 'mcp',
skill: 'skill',
subagent: 'subagent'
}
const mentionTypePattern = Object.values(mentionTypePrefixMap).join('|')
const mentionTokenRegex = new RegExp(`@(${mentionTypePattern}):\\S+`, 'g')
export const formatMentionToken = (type, value) => {
const prefix = mentionTypePrefixMap[type] || type
return `@${prefix}:${value}`
}
export const parseMentionText = (text = '') => {
const value = String(text || '')
const segments = []
let lastIndex = 0
mentionTokenRegex.lastIndex = 0
for (const match of value.matchAll(mentionTokenRegex)) {
const raw = match[0]
const start = match.index ?? 0
const end = start + raw.length
if (start > lastIndex) {
segments.push({
kind: 'text',
text: value.slice(lastIndex, start),
start: lastIndex,
end: start
})
}
const type = match[1]
const prefix = `@${type}:`
segments.push({
kind: 'mention',
raw,
type,
value: raw.slice(prefix.length),
start,
end
})
lastIndex = end
}
if (lastIndex < value.length) {
segments.push({
kind: 'text',
text: value.slice(lastIndex),
start: lastIndex,
end: value.length
})
}
return segments
}
const setMentionLabel = (labels, type, value, label) => {
const rawValue = String(value || '').trim()
const displayLabel = String(label || '').trim()
if (!rawValue || !displayLabel) return
labels[`${type}:${rawValue}`] = displayLabel
}
export const buildMentionDisplayLabels = (mention = {}) => {
const labels = {}
;(mention.knowledgeBases || []).forEach((kb) => {
const label = kb?.name || kb?.label || kb?.kb_id || kb?.value || ''
setMentionLabel(labels, 'knowledge', kb?.name, label)
setMentionLabel(labels, 'knowledge', kb?.label, label)
setMentionLabel(labels, 'knowledge', kb?.kb_id, label)
setMentionLabel(labels, 'knowledge', kb?.value, label)
})
;(mention.mcps || []).forEach((mcp) => {
const label = mcp?.name || mcp?.label || mcp?.slug || mcp?.id || mcp?.value || ''
setMentionLabel(labels, 'mcp', mcp?.slug, label)
setMentionLabel(labels, 'mcp', mcp?.id, label)
setMentionLabel(labels, 'mcp', mcp?.value, label)
setMentionLabel(labels, 'mcp', mcp?.name, label)
})
;(mention.skills || []).forEach((skill) => {
const label = skill?.name || skill?.label || skill?.slug || skill?.id || skill?.value || ''
setMentionLabel(labels, 'skill', skill?.slug, label)
setMentionLabel(labels, 'skill', skill?.id, label)
setMentionLabel(labels, 'skill', skill?.value, label)
setMentionLabel(labels, 'skill', skill?.name, label)
})
;(mention.subagents || []).forEach((subagent) => {
const label = subagent?.name || subagent?.label || subagent?.id || subagent?.value || ''
setMentionLabel(labels, 'subagent', subagent?.id, label)
setMentionLabel(labels, 'subagent', subagent?.value, label)
setMentionLabel(labels, 'subagent', subagent?.slug, label)
setMentionLabel(labels, 'subagent', subagent?.name, label)
})
return labels
}
export const getMentionDisplayLabel = (type, value, displayLabels = {}) => {
const mappedLabel = displayLabels[`${type}:${value}`]
if (mappedLabel) return mappedLabel
if (type === 'file') {
const normalizedPath = String(value || '').replace(/\/+$/, '')
return getDisplayFileName(normalizedPath || value, '文件')
}
return String(value || '').trim() || type
}
export const findActiveMentionQuery = (text = '', rawCaretOffset = 0) => {
const value = String(text || '')
const offset = Math.max(0, Math.min(rawCaretOffset, value.length))
const segments = parseMentionText(value)
const mentionAtCursor = segments.find(
(segment) => segment.kind === 'mention' && offset > segment.start && offset <= segment.end
)
if (mentionAtCursor) return null
const textBeforeCursor = value.slice(0, offset)
const atIndex = textBeforeCursor.lastIndexOf('@')
if (atIndex === -1) return null
const query = textBeforeCursor.slice(atIndex + 1)
if (query.includes('@') || query.includes(':') || /\s/.test(query)) return null
return { start: atIndex, end: offset, query }
}
export const replaceRawRange = (text = '', start = 0, end = start, replacement = '') => {
const value = String(text || '')
const safeStart = Math.max(0, Math.min(start, value.length))
const safeEnd = Math.max(safeStart, Math.min(end, value.length))
return value.slice(0, safeStart) + replacement + value.slice(safeEnd)
}
export const expandMentionDeletionRange = (text = '', start = 0, end = start, direction = 'backward') => {
const value = String(text || '')
const safeStart = Math.max(0, Math.min(start, value.length))
const safeEnd = Math.max(safeStart, Math.min(end, value.length))
const mentions = parseMentionText(value).filter((segment) => segment.kind === 'mention')
if (safeStart !== safeEnd) {
const touchedMentions = mentions.filter(
(segment) => segment.start < safeEnd && segment.end > safeStart
)
if (!touchedMentions.length) return null
return {
start: Math.min(safeStart, ...touchedMentions.map((segment) => segment.start)),
end: Math.max(safeEnd, ...touchedMentions.map((segment) => segment.end))
}
}
const cursor = safeStart
if (direction === 'forward') {
const mention = mentions.find((segment) => cursor >= segment.start && cursor < segment.end)
if (!mention) return null
const includeTrailingSpace = value[mention.end] === ' '
return {
start: mention.start,
end: includeTrailingSpace ? mention.end + 1 : mention.end
}
}
const mention = mentions.find((segment) => cursor > segment.start && cursor <= segment.end)
if (mention) {
return { start: mention.start, end: mention.end }
}
return null
}