mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 17:59:09 +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 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(
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -332,7 +332,8 @@
|
||||
},
|
||||
"new_topic": "新话题 {{Command}}",
|
||||
"pause": "暂停",
|
||||
"placeholder": "在这里输入消息,按 {{key}} 发送...",
|
||||
"placeholder": "在这里输入消息,按 {{key}} 发送 - @ 选择模型, / 选择工具",
|
||||
"placeholder_without_triggers": "在这里输入消息,按 {{key}} 发送",
|
||||
"send": "发送",
|
||||
"settings": "设置",
|
||||
"thinking": {
|
||||
|
||||
@ -332,7 +332,8 @@
|
||||
},
|
||||
"new_topic": "新話題 {{Command}}",
|
||||
"pause": "暫停",
|
||||
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送...",
|
||||
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送 - @ 選擇模型,/ 包含工具",
|
||||
"placeholder_without_triggers": "在此輸入您的訊息,按 {{key}} 傳送",
|
||||
"send": "傳送",
|
||||
"settings": "設定",
|
||||
"thinking": {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user