feat: support hover hightlight cmd

This commit is contained in:
Joel
2026-03-26 14:25:11 +08:00
parent 1c01bd773b
commit f8e5421a01
6 changed files with 102 additions and 68 deletions

View File

@ -1,16 +1,18 @@
import type { ReactElement } from 'react'
import { render, screen } from '@testing-library/react'
import { render } from '@testing-library/react'
import SandboxPlaceholder from '../sandbox-placeholder'
vi.mock('react-i18next', () => ({
Trans: ({ i18nKey, components = [] }: {
i18nKey: string
components?: ReactElement[]
}) => (
<div data-i18n-key={i18nKey} data-testid="sandbox-placeholder-trans">
{components}
</div>
),
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'promptEditor.placeholderSandboxPrefix': 'Write instructions here, ',
'promptEditor.placeholderSandboxInsert': 'insert',
'promptEditor.placeholderSandboxSeparator': ', ',
'promptEditor.placeholderSandboxTools': 'tools',
}
return translations[key] ?? key
},
}),
}))
describe('SandboxPlaceholder', () => {
@ -24,10 +26,9 @@ describe('SandboxPlaceholder', () => {
const { container } = render(<SandboxPlaceholder isSupportSandbox={false} />)
expect(container).toBeEmptyDOMElement()
expect(screen.queryByTestId('sandbox-placeholder-trans')).not.toBeInTheDocument()
})
it('should render slash and insert tokens when tool blocks are disabled', () => {
it('should render only the insert pair when tool blocks are disabled', () => {
const { container } = render(
<SandboxPlaceholder
disableToolBlocks
@ -35,25 +36,33 @@ describe('SandboxPlaceholder', () => {
/>,
)
expect(screen.getByTestId('sandbox-placeholder-trans')).toHaveAttribute('data-i18n-key', 'promptEditor.placeholderSandboxNoTools')
const spans = container.querySelectorAll('span')
expect(spans).toHaveLength(2)
expect(spans[0]).toHaveClass('inline-flex', 'bg-components-kbd-bg-gray', 'system-kbd')
expect(spans[1]).toHaveClass('border-b', 'border-dotted', 'border-current')
expect(container).toHaveTextContent('Write instructions here, /insert')
expect(container.querySelector('.sandbox-placeholder-pair-insert')).toBeInTheDocument()
expect(container.querySelector('.sandbox-placeholder-pair-tools')).not.toBeInTheDocument()
expect(container.querySelector('.sandbox-placeholder-action-insert')).toHaveClass(
'pointer-events-auto',
'border-dotted',
)
})
it('should render slash insert at and tools tokens when tool blocks are enabled', () => {
it('should render both insert and tools pairs with linked hover classes when tool blocks are enabled', () => {
const { container } = render(<SandboxPlaceholder isSupportSandbox />)
expect(screen.getByTestId('sandbox-placeholder-trans')).toHaveAttribute('data-i18n-key', 'promptEditor.placeholderSandbox')
const spans = container.querySelectorAll('span')
expect(spans).toHaveLength(4)
expect(spans[0]).toHaveClass('inline-flex', 'bg-components-kbd-bg-gray', 'system-kbd')
expect(spans[1]).toHaveClass('border-b', 'border-dotted', 'border-current')
expect(spans[2]).toHaveClass('inline-flex', 'bg-components-kbd-bg-gray', 'system-kbd')
expect(spans[3]).toHaveClass('border-b', 'border-dotted', 'border-current')
expect(container).toHaveTextContent('Write instructions here, /insert, @tools')
expect(container.querySelector('.sandbox-placeholder-kbd-insert')).toHaveTextContent('/')
expect(container.querySelector('.sandbox-placeholder-kbd-tools')).toHaveTextContent('@')
expect(container.querySelector('.sandbox-placeholder-action-insert')).toHaveTextContent('insert')
expect(container.querySelector('.sandbox-placeholder-action-tools')).toHaveTextContent('tools')
expect(container.querySelector('.sandbox-placeholder-pair-insert')).toHaveClass(
'has-[.sandbox-placeholder-action-insert:hover]:[&_.sandbox-placeholder-kbd-insert]:bg-state-accent-hover-alt',
'has-[.sandbox-placeholder-action-insert:hover]:[&_.sandbox-placeholder-action-insert]:bg-state-accent-hover',
'has-[.sandbox-placeholder-action-insert:hover]:[&_.sandbox-placeholder-action-insert]:text-text-accent-secondary',
)
expect(container.querySelector('.sandbox-placeholder-pair-tools')).toHaveClass(
'has-[.sandbox-placeholder-action-tools:hover]:[&_.sandbox-placeholder-kbd-tools]:bg-state-accent-hover-alt',
'has-[.sandbox-placeholder-action-tools:hover]:[&_.sandbox-placeholder-action-tools]:bg-state-accent-hover',
'has-[.sandbox-placeholder-action-tools:hover]:[&_.sandbox-placeholder-action-tools]:text-text-accent-secondary',
)
})
})
})

View File

@ -1,22 +1,38 @@
import type { FC, PropsWithChildren, ReactElement } from 'react'
import { Trans } from 'react-i18next'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type SandboxPlaceholderTokenProps = PropsWithChildren<{
variant: 'kbd' | 'action'
}>
const SandboxPlaceholderToken: FC<SandboxPlaceholderTokenProps> = ({ variant, children }) => {
if (variant === 'kbd') {
return (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 text-text-tertiary system-kbd">
{children}
</span>
)
}
type SandboxPlaceholderTokenProps = {
actionLabel?: string
shortcut: '/' | '@'
}
const SandboxPlaceholderToken: FC<SandboxPlaceholderTokenProps> = ({
actionLabel,
shortcut,
}) => {
return (
<span className="border-b border-dotted border-current">
{children}
<span
className={cn(
'inline-flex cursor-pointer items-center gap-1 text-text-tertiary hover:text-components-button-secondary-accent-text',
'group/placeholder-token',
)}
>
<span
className={cn(
'inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 system-kbd',
'group-hover/placeholder-token:bg-components-button-secondary-accent-text-disabled',
)}
>
{shortcut}
</span>
<span
className={cn(
'pointer-events-auto border-b border-dotted border-current px-0.5 transition-colors',
)}
>
{actionLabel}
</span>
</span>
)
}
@ -30,27 +46,28 @@ const SandboxPlaceholder: FC<SandboxPlaceholderProps> = ({
disableToolBlocks,
isSupportSandbox,
}) => {
const { t } = useTranslation()
if (!isSupportSandbox)
return null
const components: ReactElement[] = [
<SandboxPlaceholderToken key="slash" variant="kbd" />,
<SandboxPlaceholderToken key="insert" variant="action" />,
]
if (!disableToolBlocks) {
components.push(
<SandboxPlaceholderToken key="at" variant="kbd" />,
<SandboxPlaceholderToken key="tools" variant="action" />,
)
}
return (
<Trans
i18nKey={disableToolBlocks ? 'promptEditor.placeholderSandboxNoTools' : 'promptEditor.placeholderSandbox'}
ns="common"
components={components}
/>
<span>
{t('promptEditor.placeholderSandboxPrefix', { ns: 'common' })}
<SandboxPlaceholderToken
shortcut="/"
actionLabel={t('promptEditor.placeholderSandboxInsert', { ns: 'common' })}
/>
{!disableToolBlocks && (
<>
{t('promptEditor.placeholderSandboxSeparator', { ns: 'common' })}
<SandboxPlaceholderToken
shortcut="@"
actionLabel={t('promptEditor.placeholderSandboxTools', { ns: 'common' })}
/>
</>
)}
</span>
)
}

View File

@ -549,8 +549,10 @@
"promptEditor.history.modal.title": "EXAMPLE",
"promptEditor.history.modal.user": "Hello",
"promptEditor.placeholder": "Write your prompt word here, enter '{' to insert a variable, enter '/' to insert a prompt content block",
"promptEditor.placeholderSandbox": "Write instructions here, <0>/</0> <1>insert</1>, <2>@</2> <3>tools</3>",
"promptEditor.placeholderSandboxNoTools": "Write instructions here, <0>/</0> <1>insert</1>",
"promptEditor.placeholderSandboxInsert": "insert",
"promptEditor.placeholderSandboxPrefix": "Write instructions here, ",
"promptEditor.placeholderSandboxSeparator": ", ",
"promptEditor.placeholderSandboxTools": "tools",
"promptEditor.query.item.desc": "Insert user query template",
"promptEditor.query.item.title": "Query",
"promptEditor.requestURL.item.desc": "Insert request URL",

View File

@ -534,8 +534,10 @@
"promptEditor.history.modal.title": "例",
"promptEditor.history.modal.user": "こんにちは",
"promptEditor.placeholder": "ここにプロンプトワードを入力してください。変数を挿入するには「{」を、プロンプトコンテンツブロックを挿入するには「/」を入力します。",
"promptEditor.placeholderSandbox": "ここに指示を書いて、<0>/</0> <1>挿入</1>、<2>@</2> <3>ツール</3>",
"promptEditor.placeholderSandboxNoTools": "ここに指示を書いて、<0>/</0> <1>挿入</1>",
"promptEditor.placeholderSandboxInsert": "挿入",
"promptEditor.placeholderSandboxPrefix": "ここに指示を書いて、",
"promptEditor.placeholderSandboxSeparator": "、",
"promptEditor.placeholderSandboxTools": "ツール",
"promptEditor.query.item.desc": "ユーザークエリテンプレートを挿入",
"promptEditor.query.item.title": "クエリ",
"promptEditor.requestURL.item.desc": "リクエストURLを挿入",

View File

@ -549,8 +549,10 @@
"promptEditor.history.modal.title": "示例",
"promptEditor.history.modal.user": "你好",
"promptEditor.placeholder": "在这里写你的提示词,输入'{' 插入变量、输入'/' 插入提示内容块",
"promptEditor.placeholderSandbox": "在这里写你的指令,<0>/</0> <1>插入</1><2>@</2> <3>工具</3>",
"promptEditor.placeholderSandboxNoTools": "在这里写你的指令,<0>/</0> <1>插入</1>",
"promptEditor.placeholderSandboxInsert": "插入",
"promptEditor.placeholderSandboxPrefix": "在这里写你的指令,",
"promptEditor.placeholderSandboxSeparator": "",
"promptEditor.placeholderSandboxTools": "工具",
"promptEditor.query.item.desc": "插入用户查询模板",
"promptEditor.query.item.title": "查询内容",
"promptEditor.requestURL.item.desc": "插入请求 URL",

View File

@ -509,8 +509,10 @@
"promptEditor.history.modal.title": "示例",
"promptEditor.history.modal.user": "你好",
"promptEditor.placeholder": "在這裡寫你的提示詞,輸入'{' 插入變數、輸入'/' 插入提示內容塊",
"promptEditor.placeholderSandbox": "在這裡寫你的指令,<0>/</0> <1>插入</1><2>@</2> <3>工具</3>",
"promptEditor.placeholderSandboxNoTools": "在這裡寫你的指令,<0>/</0> <1>插入</1>",
"promptEditor.placeholderSandboxInsert": "插入",
"promptEditor.placeholderSandboxPrefix": "在這裡寫你的指令,",
"promptEditor.placeholderSandboxSeparator": "",
"promptEditor.placeholderSandboxTools": "工具",
"promptEditor.query.item.desc": "插入使用者查詢模板",
"promptEditor.query.item.title": "查詢內容",
"promptEditor.requestURL.item.desc": "插入請求 URL",