fix: support leadingspace to avoid normal text (#10264)

* fix: support leadingspace to avoid normal text

* Close QuickPanel when no search results found

Add automatic closing of QuickPanel when search yields no results for
single-select input triggers, preventing users from getting stuck with
empty result lists.

* fix: reopen quick panel while editing trigger text

* fix: hide quick trigger hints when disabled

* Update zh-tw.json
This commit is contained in:
SuYao 2025-09-22 00:11:27 +08:00 committed by GitHub
parent 4f8507036a
commit 67a6a6a445
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 116 additions and 36 deletions

View File

@ -158,15 +158,22 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
const cursorPosition = textArea.selectionStart ?? 0
const textBeforeCursor = textArea.value.slice(0, cursorPosition)
// 查找最后一个 @ 或 / 符号的位置
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
const lastSlashIndex = textBeforeCursor.lastIndexOf('/')
const lastSymbolIndex = Math.max(lastAtIndex, lastSlashIndex)
// 查找末尾最近的触发符号(@ 或 /),允许位于文本起始或空格后
const match = textBeforeCursor.match(/(^| )([@/][^\s]*)$/)
if (!match) return
if (lastSymbolIndex === -1) return
const matchIndex = match.index ?? -1
if (matchIndex === -1) return
const boundarySegment = match[1] ?? ''
const symbolSegment = match[2] ?? ''
if (!symbolSegment) return
const boundaryStart = matchIndex
const symbolStart = boundaryStart + boundarySegment.length
// 根据 includeSymbol 决定是否删除符号
const deleteStart = includeSymbol ? lastSymbolIndex : lastSymbolIndex + 1
const deleteStart = includeSymbol ? boundaryStart : symbolStart + 1
const deleteEnd = cursorPosition
if (deleteStart >= deleteEnd) return
@ -203,7 +210,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
if (textArea) {
setInputText(textArea.value)
}
} else if (action && !['outsideclick', 'esc', 'enter_empty'].includes(action)) {
} else if (action && !['outsideclick', 'esc', 'enter_empty', 'no_result'].includes(action)) {
clearSearchText(true)
}
},
@ -533,6 +540,18 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
const visibleNonPinnedCount = useMemo(() => list.filter((i) => !i.alwaysVisible).length, [list])
const collapsed = hasSearchText && visibleNonPinnedCount === 0
useEffect(() => {
if (!ctx.isVisible) return
if (!collapsed) return
if (ctx.triggerInfo?.type !== 'input') return
if (ctx.multiple) return
const trimmedSearch = searchText.replace(/^[/@]/, '').trim()
if (!trimmedSearch) return
handleClose('no_result')
}, [collapsed, ctx.isVisible, ctx.triggerInfo, ctx.multiple, handleClose, searchText])
const estimateSize = useCallback(() => ITEM_HEIGHT, [])
const rowRenderer = useCallback(

View File

@ -332,7 +332,8 @@
},
"new_topic": "New Topic {{Command}}",
"pause": "Pause",
"placeholder": "Type your message here, press {{key}} to send...",
"placeholder": "Type your message here, press {{key}} to send - @ to Select Model, / to Include Tools",
"placeholder_without_triggers": "Type your message here, press {{key}} to send",
"send": "Send",
"settings": "Settings",
"thinking": {

View File

@ -332,7 +332,8 @@
},
"new_topic": "新话题 {{Command}}",
"pause": "暂停",
"placeholder": "在这里输入消息,按 {{key}} 发送...",
"placeholder": "在这里输入消息,按 {{key}} 发送 - @ 选择模型, / 选择工具",
"placeholder_without_triggers": "在这里输入消息,按 {{key}} 发送",
"send": "发送",
"settings": "设置",
"thinking": {

View File

@ -332,7 +332,8 @@
},
"new_topic": "新話題 {{Command}}",
"pause": "暫停",
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送...",
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送 - @ 選擇模型,/ 包含工具",
"placeholder_without_triggers": "在此輸入您的訊息,按 {{key}} 傳送",
"send": "傳送",
"settings": "設定",
"thinking": {

View File

@ -162,6 +162,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const [tokenCount, setTokenCount] = useState(0)
const inputbarToolsRef = useRef<InputbarToolsRef>(null)
const prevTextRef = useRef(text)
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedEstimate = useCallback(
@ -178,8 +179,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
debouncedEstimate(text)
}, [text, debouncedEstimate])
useEffect(() => {
prevTextRef.current = text
}, [text])
const inputTokenCount = showInputEstimatedTokens ? tokenCount : 0
const placeholderText = enableQuickPanelTriggers
? t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })
: t('chat.input.placeholder_without_triggers', {
key: getSendMessageShortcutLabel(sendMessageShortcut),
defaultValue: t('chat.input.placeholder', {
key: getSendMessageShortcutLabel(sendMessageShortcut)
})
})
const inputEmpty = isEmpty(text.trim()) && files.length === 0
_text = text
@ -441,43 +455,91 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const newText = e.target.value
setText(newText)
const prevText = prevTextRef.current
const isDeletion = newText.length < prevText.length
const textArea = textareaRef.current?.resizableTextArea?.textArea
const cursorPosition = textArea?.selectionStart ?? 0
const cursorPosition = textArea?.selectionStart ?? newText.length
const lastSymbol = newText[cursorPosition - 1]
const previousChar = newText[cursorPosition - 2]
const isCursorAtTextStart = cursorPosition <= 1
const hasValidTriggerBoundary = previousChar === ' ' || isCursorAtTextStart
const openRootPanelAt = (position: number) => {
const quickPanelMenu =
inputbarToolsRef.current?.getQuickPanelMenu({
text: newText,
translate
}) || []
quickPanel.open({
title: t('settings.quickPanel.title'),
list: quickPanelMenu,
symbol: QuickPanelReservedSymbol.Root,
triggerInfo: {
type: 'input',
position,
originalText: newText
}
})
}
const openMentionPanelAt = (position: number) => {
inputbarToolsRef.current?.openMentionModelsPanel({
type: 'input',
position,
originalText: newText
})
}
if (enableQuickPanelTriggers && !quickPanel.isVisible) {
const textBeforeCursor = newText.slice(0, cursorPosition)
const lastRootIndex = textBeforeCursor.lastIndexOf(QuickPanelReservedSymbol.Root)
const lastMentionIndex = textBeforeCursor.lastIndexOf(QuickPanelReservedSymbol.MentionModels)
const lastTriggerIndex = Math.max(lastRootIndex, lastMentionIndex)
if (lastTriggerIndex !== -1 && cursorPosition > lastTriggerIndex) {
const triggerChar = newText[lastTriggerIndex]
const boundaryChar = newText[lastTriggerIndex - 1] ?? ''
const hasBoundary = lastTriggerIndex === 0 || /\s/.test(boundaryChar)
const searchSegment = newText.slice(lastTriggerIndex + 1, cursorPosition)
const hasSearchContent = searchSegment.trim().length > 0
if (hasBoundary && (!hasSearchContent || isDeletion)) {
if (triggerChar === QuickPanelReservedSymbol.Root) {
openRootPanelAt(lastTriggerIndex)
} else if (triggerChar === QuickPanelReservedSymbol.MentionModels) {
openMentionPanelAt(lastTriggerIndex)
}
}
}
}
// 触发符号为 '/':若当前未打开或符号不同,则切换/打开
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.Root) {
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.Root && hasValidTriggerBoundary) {
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
quickPanel.close('switch-symbol')
}
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
const quickPanelMenu =
inputbarToolsRef.current?.getQuickPanelMenu({
text: newText,
translate
}) || []
quickPanel.open({
title: t('settings.quickPanel.title'),
list: quickPanelMenu,
symbol: QuickPanelReservedSymbol.Root
})
openRootPanelAt(cursorPosition - 1)
}
}
// 触发符号为 '@':若当前未打开或符号不同,则切换/打开
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.MentionModels) {
if (
enableQuickPanelTriggers &&
lastSymbol === QuickPanelReservedSymbol.MentionModels &&
hasValidTriggerBoundary
) {
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
quickPanel.close('switch-symbol')
}
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
inputbarToolsRef.current?.openMentionModelsPanel({
type: 'input',
position: cursorPosition - 1,
originalText: newText
})
openMentionPanelAt(cursorPosition - 1)
}
}
prevTextRef.current = newText
},
[enableQuickPanelTriggers, quickPanel, t, translate]
)
@ -783,11 +845,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
value={text}
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={
isTranslating
? t('chat.input.translating')
: t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })
}
placeholder={isTranslating ? t('chat.input.translating') : placeholderText}
autoFocus
variant="borderless"
spellCheck={enableSpellCheck}

View File

@ -89,7 +89,7 @@ const MentionModelsButton: FC<Props> = ({
// 兜底:使用打开时的 position若存在按空白边界删除
if (typeof fallbackPosition === 'number' && currentText[fallbackPosition] === '@') {
let endPos = fallbackPosition + 1
while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') {
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
endPos++
}
return currentText.slice(0, fallbackPosition) + currentText.slice(endPos)
@ -98,7 +98,7 @@ const MentionModelsButton: FC<Props> = ({
}
let endPos = start + 1
while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') {
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
endPos++
}
return currentText.slice(0, start) + currentText.slice(endPos)