Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2356,6 +2356,11 @@ body.image-preview-open {
overflow: hidden;
}

.composer-card:focus-visible {
outline: 2px solid rgba(77, 168, 255, 0.78);
outline-offset: -2px;
}

.composer-shell {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -3185,3 +3190,102 @@ body[data-route='/'] .social-link-button:focus-visible::before {
.browser-assist-form {
max-width: 620px;
}

.keyboard-shortcuts-settings {
max-width: 780px;
}

.keyboard-shortcuts-heading h4 {
margin: 24px 0 4px;
}

.keyboard-shortcuts-heading .ant-typography {
margin-bottom: 16px;
}

.keyboard-shortcut-list {
overflow: hidden;
border: 1px solid var(--app-border);
border-radius: 10px;
background: var(--app-bg-panel-strong);
}

.keyboard-shortcut-row {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(120px, auto) auto;
align-items: center;
gap: 16px;
padding: 14px 16px;
border-bottom: 1px solid var(--app-border-soft);
}

.keyboard-shortcut-row:last-child {
border-bottom: 0;
}

.keyboard-shortcut-copy {
min-width: 0;
display: grid;
gap: 3px;
}

.keyboard-shortcut-key {
min-width: 108px;
padding: 5px 9px;
border: 1px solid var(--app-border);
border-bottom-color: var(--app-border-strong);
border-radius: 6px;
background: var(--app-bg-muted);
color: var(--app-text);
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
font-size: 12px;
font-weight: 700;
line-height: 1.4;
text-align: center;
white-space: nowrap;
}

.keyboard-shortcut-key.is-empty {
color: var(--app-text-muted);
font-weight: 500;
}

.keyboard-shortcut-actions {
justify-self: end;
}

.keyboard-shortcuts-error {
margin-top: 12px;
}

.keyboard-shortcuts-footer {
margin-top: 16px;
}

.keyboard-shortcut-live {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

@media (max-width: 720px) {
.keyboard-shortcut-row {
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
}

.keyboard-shortcut-key {
min-width: 96px;
}

.keyboard-shortcut-actions {
grid-column: 1 / -1;
justify-self: start;
}
}
68 changes: 56 additions & 12 deletions frontend/src/components/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { ToolField } from './ToolField'
import { ChatMessageList } from './ChatMessageList'
import { ToolCallAssistPopover } from '../features/tool-call-assist/ToolCallAssistPopover'
import { normalizeChatText } from '../lib/chat-format'
import { formatKeyboardShortcutBinding, type KeyboardShortcutConfig } from '../features/keyboard-shortcuts/shortcuts'
import type { ComposerKeyboardAction } from '../hooks/chatWorkspace/composerKeyboard'
import type {
ComposerMode,
BuiltinToolOption,
Expand Down Expand Up @@ -65,10 +67,11 @@ type ChatPaneProps = {
composer: string
composerMode: ComposerMode
draftBuffer: string
handleComposerKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void
handleComposerKeyDown: (event: KeyboardEvent<HTMLElement>) => ComposerKeyboardAction['type']
isMobile: boolean
isWaitingForUser: boolean
keyboardOffset: number
keyboardShortcuts: KeyboardShortcutConfig
messagesLoading: boolean
onDraft: () => void | Promise<void>
onAutomationRecording: (action: 'start' | 'stop' | 'cancel') => void | Promise<void>
Expand Down Expand Up @@ -119,6 +122,7 @@ export function ChatPane(props: ChatPaneProps) {
isMobile,
isWaitingForUser,
keyboardOffset,
keyboardShortcuts,
messagesLoading,
onDraft,
onAutomationRecording,
Expand Down Expand Up @@ -264,6 +268,38 @@ export function ChatPane(props: ChatPaneProps) {
{ label: 'summery 模式', value: 'summery' },
{ label: 'reasoning 模式', value: 'reasoning' },
]
const shortcutCopy = (action: keyof KeyboardShortcutConfig['bindings'], label: string) => {
const binding = keyboardShortcuts.bindings[action]
return binding ? `${formatKeyboardShortcutBinding(binding)} ${label}` : ''
}
const modeCycleShortcut = shortcutCopy('cycle_mode', '切换类型')
const answerShortcutHint = [
shortcutCopy('stream', '流式输出'),
shortcutCopy('newline', '换行'),
shortcutCopy('complete', '结束输出'),
shortcutCopy('restore_draft', '继续编辑'),
modeCycleShortcut,
].filter(Boolean).join(',')
const thinkingShortcutHint = [
shortcutCopy('stream', '流式输出'),
shortcutCopy('newline', '换行'),
modeCycleShortcut,
].filter(Boolean).join(',')

function handleComposerAreaKeyDown(event: KeyboardEvent<HTMLElement>) {
const action = handleComposerKeyDown(event)
if (action !== 'cycle_mode') return
window.requestAnimationFrame(() => {
const editor = composerCardRef.current?.querySelector<HTMLTextAreaElement>(
'textarea[data-composer-editor="true"]:not(:disabled)',
)
if (editor) {
editor.focus()
} else {
composerCardRef.current?.focus()
}
})
}

return (
<div className="chat-pane" style={paneStyle}>
Expand Down Expand Up @@ -299,7 +335,15 @@ export function ChatPane(props: ChatPaneProps) {
/>
</div>

<Card ref={composerCardRef} className="composer-card" style={composerStyle}>
<Card
ref={composerCardRef}
className="composer-card"
style={composerStyle}
role="group"
aria-label="回复编辑区快捷键区域"
tabIndex={-1}
onKeyDown={handleComposerAreaKeyDown}
>
<div className="composer-shell">
<Space direction="vertical" size={12} className="composer-stack">
{draftBuffer && (
Expand Down Expand Up @@ -539,16 +583,16 @@ export function ChatPane(props: ChatPaneProps) {
<Typography.Text className="thinking-panel-hint">
当前会以{' '}
{reasoningStreamMode === 'reasoning' ? 'reasoning' : 'summery'}
输出给调用方 · Enter 流式输出,Shift+Enter 换行
输出给调用方{thinkingShortcutHint ? ` · ${thinkingShortcutHint}` : ''}
</Typography.Text>
</div>
<TextArea
value={thinkingText}
onChange={(event) => setThinkingText(event.target.value)}
onKeyDown={handleComposerKeyDown}
data-composer-editor="true"
placeholder={
isWaitingForUser
? '输入思考过程。按 Enter 会立刻流式输出给调用方(和正文一样),Shift+Enter 换行。'
? `输入思考过程。${thinkingShortcutHint || '可使用下方按钮输出。'}`
: '当前没有等待中的 user 请求。'
}
autoSize={{ minRows: 4, maxRows: 10 }}
Expand All @@ -562,10 +606,10 @@ export function ChatPane(props: ChatPaneProps) {
<TextArea
value={composer}
onChange={(event) => setComposer(event.target.value)}
onKeyDown={handleComposerKeyDown}
data-composer-editor="true"
placeholder={
isWaitingForUser
? '输入你作为 assistant 的回复。Enter 流式输出,Shift+Enter 换行,Ctrl/⌘+Enter 结束输出。'
? `输入你作为 assistant 的回复。${answerShortcutHint || '可使用下方按钮输出。'}`
: '当前没有等待中的 user 请求。'
}
autoSize={{ minRows: 4, maxRows: 10 }}
Expand All @@ -581,16 +625,16 @@ export function ChatPane(props: ChatPaneProps) {
? '正在发送并等待服务端同步草稿…'
: isWaitingForUser
? composerMode === 'assistant_message'
? 'Enter 流式输出,Shift+Enter 换行,Ctrl/⌘+Enter 结束。片段会保留在本轮回复里。'
? `${answerShortcutHint || '可使用右侧按钮输出'}。片段会保留在本轮回复里。`
: composerMode === 'thinking'
? `Enter 流式输出思考(${
? `${thinkingShortcutHint || '可使用左侧按钮输出思考'}(${
isResponsesConversation && reasoningStreamMode === 'reasoning'
? 'reasoning'
: 'summery'
}),Shift+Enter 换行。思考不会结束这一轮。`
})。思考不会结束这一轮。`
: composerMode === 'builtin_tool'
? '内置工具会输出 Responses 官方内置工具事件,不会结束这一轮。'
: 'Tool Call 模式会根据 schema 组装参数 JSON,点击左侧按钮会直接输出一个 function_call item。'
? `内置工具会输出 Responses 官方内置工具事件,不会结束这一轮${modeCycleShortcut ? ` · ${modeCycleShortcut}` : ''}。`
: `Tool Call 模式会根据 schema 组装参数 JSON,点击左侧按钮会直接输出一个 function_call item${modeCycleShortcut ? ` · ${modeCycleShortcut}` : ''}。`
: '没有新的 user 请求时不能输出回复。'}
</Typography.Text>
<Space>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/WorkspaceRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function WorkspaceRoute() {
isMobile={isMobile}
isWaitingForUser={workspace.isWaitingForUser}
keyboardOffset={workspace.keyboardOffset}
keyboardShortcuts={workspace.keyboardShortcuts}
messagesLoading={workspace.messagesLoading}
onDraft={workspace.handleDraft}
onAutomationRecording={workspace.handleAutomationRecording}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/settings/AutomationRulesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'

import type { AutomationRule } from '../../types/chat'

type AutomationRulesPanelProps = {
export type AutomationRulesPanelProps = {
automationRules: AutomationRule[]
onCreateAutomationRule: () => void | Promise<void>
onDeleteAutomationRule: (ruleId: string) => void | Promise<void>
Expand Down
Loading