mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +08:00
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:
parent
4f8507036a
commit
67a6a6a445
@ -158,15 +158,22 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
const cursorPosition = textArea.selectionStart ?? 0
|
const cursorPosition = textArea.selectionStart ?? 0
|
||||||
const textBeforeCursor = textArea.value.slice(0, cursorPosition)
|
const textBeforeCursor = textArea.value.slice(0, cursorPosition)
|
||||||
|
|
||||||
// 查找最后一个 @ 或 / 符号的位置
|
// 查找末尾最近的触发符号(@ 或 /),允许位于文本起始或空格后
|
||||||
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
|
const match = textBeforeCursor.match(/(^| )([@/][^\s]*)$/)
|
||||||
const lastSlashIndex = textBeforeCursor.lastIndexOf('/')
|
if (!match) return
|
||||||
const lastSymbolIndex = Math.max(lastAtIndex, lastSlashIndex)
|
|
||||||
|
|
||||||
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 决定是否删除符号
|
// 根据 includeSymbol 决定是否删除符号
|
||||||
const deleteStart = includeSymbol ? lastSymbolIndex : lastSymbolIndex + 1
|
const deleteStart = includeSymbol ? boundaryStart : symbolStart + 1
|
||||||
const deleteEnd = cursorPosition
|
const deleteEnd = cursorPosition
|
||||||
|
|
||||||
if (deleteStart >= deleteEnd) return
|
if (deleteStart >= deleteEnd) return
|
||||||
@ -203,7 +210,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
if (textArea) {
|
if (textArea) {
|
||||||
setInputText(textArea.value)
|
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)
|
clearSearchText(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -533,6 +540,18 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
const visibleNonPinnedCount = useMemo(() => list.filter((i) => !i.alwaysVisible).length, [list])
|
const visibleNonPinnedCount = useMemo(() => list.filter((i) => !i.alwaysVisible).length, [list])
|
||||||
const collapsed = hasSearchText && visibleNonPinnedCount === 0
|
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 estimateSize = useCallback(() => ITEM_HEIGHT, [])
|
||||||
|
|
||||||
const rowRenderer = useCallback(
|
const rowRenderer = useCallback(
|
||||||
|
|||||||
@ -332,7 +332,8 @@
|
|||||||
},
|
},
|
||||||
"new_topic": "New Topic {{Command}}",
|
"new_topic": "New Topic {{Command}}",
|
||||||
"pause": "Pause",
|
"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",
|
"send": "Send",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"thinking": {
|
"thinking": {
|
||||||
|
|||||||
@ -332,7 +332,8 @@
|
|||||||
},
|
},
|
||||||
"new_topic": "新话题 {{Command}}",
|
"new_topic": "新话题 {{Command}}",
|
||||||
"pause": "暂停",
|
"pause": "暂停",
|
||||||
"placeholder": "在这里输入消息,按 {{key}} 发送...",
|
"placeholder": "在这里输入消息,按 {{key}} 发送 - @ 选择模型, / 选择工具",
|
||||||
|
"placeholder_without_triggers": "在这里输入消息,按 {{key}} 发送",
|
||||||
"send": "发送",
|
"send": "发送",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"thinking": {
|
"thinking": {
|
||||||
|
|||||||
@ -332,7 +332,8 @@
|
|||||||
},
|
},
|
||||||
"new_topic": "新話題 {{Command}}",
|
"new_topic": "新話題 {{Command}}",
|
||||||
"pause": "暫停",
|
"pause": "暫停",
|
||||||
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送...",
|
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送 - @ 選擇模型,/ 包含工具",
|
||||||
|
"placeholder_without_triggers": "在此輸入您的訊息,按 {{key}} 傳送",
|
||||||
"send": "傳送",
|
"send": "傳送",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"thinking": {
|
"thinking": {
|
||||||
|
|||||||
@ -162,6 +162,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const [tokenCount, setTokenCount] = useState(0)
|
const [tokenCount, setTokenCount] = useState(0)
|
||||||
|
|
||||||
const inputbarToolsRef = useRef<InputbarToolsRef>(null)
|
const inputbarToolsRef = useRef<InputbarToolsRef>(null)
|
||||||
|
const prevTextRef = useRef(text)
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedEstimate = useCallback(
|
const debouncedEstimate = useCallback(
|
||||||
@ -178,8 +179,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
debouncedEstimate(text)
|
debouncedEstimate(text)
|
||||||
}, [text, debouncedEstimate])
|
}, [text, debouncedEstimate])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prevTextRef.current = text
|
||||||
|
}, [text])
|
||||||
|
|
||||||
const inputTokenCount = showInputEstimatedTokens ? tokenCount : 0
|
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
|
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
@ -441,43 +455,91 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const newText = e.target.value
|
const newText = e.target.value
|
||||||
setText(newText)
|
setText(newText)
|
||||||
|
|
||||||
|
const prevText = prevTextRef.current
|
||||||
|
const isDeletion = newText.length < prevText.length
|
||||||
|
|
||||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
const cursorPosition = textArea?.selectionStart ?? 0
|
const cursorPosition = textArea?.selectionStart ?? newText.length
|
||||||
const lastSymbol = newText[cursorPosition - 1]
|
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) {
|
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
|
||||||
quickPanel.close('switch-symbol')
|
quickPanel.close('switch-symbol')
|
||||||
}
|
}
|
||||||
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
|
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
|
||||||
const quickPanelMenu =
|
openRootPanelAt(cursorPosition - 1)
|
||||||
inputbarToolsRef.current?.getQuickPanelMenu({
|
|
||||||
text: newText,
|
|
||||||
translate
|
|
||||||
}) || []
|
|
||||||
|
|
||||||
quickPanel.open({
|
|
||||||
title: t('settings.quickPanel.title'),
|
|
||||||
list: quickPanelMenu,
|
|
||||||
symbol: QuickPanelReservedSymbol.Root
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发符号为 '@':若当前未打开或符号不同,则切换/打开
|
// 触发符号为 '@':若当前未打开或符号不同,则切换/打开
|
||||||
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.MentionModels) {
|
if (
|
||||||
|
enableQuickPanelTriggers &&
|
||||||
|
lastSymbol === QuickPanelReservedSymbol.MentionModels &&
|
||||||
|
hasValidTriggerBoundary
|
||||||
|
) {
|
||||||
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
|
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
|
||||||
quickPanel.close('switch-symbol')
|
quickPanel.close('switch-symbol')
|
||||||
}
|
}
|
||||||
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
|
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
|
||||||
inputbarToolsRef.current?.openMentionModelsPanel({
|
openMentionPanelAt(cursorPosition - 1)
|
||||||
type: 'input',
|
|
||||||
position: cursorPosition - 1,
|
|
||||||
originalText: newText
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevTextRef.current = newText
|
||||||
},
|
},
|
||||||
[enableQuickPanelTriggers, quickPanel, t, translate]
|
[enableQuickPanelTriggers, quickPanel, t, translate]
|
||||||
)
|
)
|
||||||
@ -783,11 +845,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
value={text}
|
value={text}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={
|
placeholder={isTranslating ? t('chat.input.translating') : placeholderText}
|
||||||
isTranslating
|
|
||||||
? t('chat.input.translating')
|
|
||||||
: t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })
|
|
||||||
}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
spellCheck={enableSpellCheck}
|
spellCheck={enableSpellCheck}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
// 兜底:使用打开时的 position(若存在),按空白边界删除
|
// 兜底:使用打开时的 position(若存在),按空白边界删除
|
||||||
if (typeof fallbackPosition === 'number' && currentText[fallbackPosition] === '@') {
|
if (typeof fallbackPosition === 'number' && currentText[fallbackPosition] === '@') {
|
||||||
let endPos = fallbackPosition + 1
|
let endPos = fallbackPosition + 1
|
||||||
while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') {
|
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
|
||||||
endPos++
|
endPos++
|
||||||
}
|
}
|
||||||
return currentText.slice(0, fallbackPosition) + currentText.slice(endPos)
|
return currentText.slice(0, fallbackPosition) + currentText.slice(endPos)
|
||||||
@ -98,7 +98,7 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let endPos = start + 1
|
let endPos = start + 1
|
||||||
while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') {
|
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
|
||||||
endPos++
|
endPos++
|
||||||
}
|
}
|
||||||
return currentText.slice(0, start) + currentText.slice(endPos)
|
return currentText.slice(0, start) + currentText.slice(endPos)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user