feat: add shortcut to home window of mini app
This commit is contained in:
parent
28c0748001
commit
91bf356c73
@ -7,7 +7,7 @@ import { uuid } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react'
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -15,7 +15,7 @@ import styled from 'styled-components'
|
||||
import ChatWindow from '../chat/ChatWindow'
|
||||
import TranslateWindow from '../translate/TranslateWindow'
|
||||
import ClipboardPreview from './components/ClipboardPreview'
|
||||
import FeatureMenus from './components/FeatureMenus'
|
||||
import FeatureMenus, { FeatureMenusRef } from './components/FeatureMenus'
|
||||
import Footer from './components/Footer'
|
||||
import InputBar from './components/InputBar'
|
||||
|
||||
@ -31,12 +31,14 @@ const HomeWindow: FC = () => {
|
||||
const { defaultModel: model } = useDefaultModel()
|
||||
const { language } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const inputBarRef = useRef<HTMLDivElement>(null)
|
||||
const featureMenusRef = useRef<FeatureMenusRef>(null)
|
||||
|
||||
const referenceText = selectedText || clipboardText || text
|
||||
|
||||
const content = isFirstMessage ? (referenceText === text ? text : `${referenceText}\n\n${text}`).trim() : text.trim()
|
||||
|
||||
const onReadClipboard = useCallback(async () => {
|
||||
const readClipboard = useCallback(async () => {
|
||||
const text = await navigator.clipboard.readText().catch(() => null)
|
||||
if (text && text !== lastClipboardText) {
|
||||
setLastClipboardText(text)
|
||||
@ -44,9 +46,24 @@ const HomeWindow: FC = () => {
|
||||
}
|
||||
}, [lastClipboardText])
|
||||
|
||||
const focusInput = () => {
|
||||
if (inputBarRef.current) {
|
||||
const input = inputBarRef.current.querySelector('input')
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onWindowShow = useCallback(async () => {
|
||||
featureMenusRef.current?.resetSelectedIndex()
|
||||
readClipboard().then()
|
||||
focusInput()
|
||||
}, [readClipboard])
|
||||
|
||||
useEffect(() => {
|
||||
onReadClipboard()
|
||||
}, [onReadClipboard])
|
||||
readClipboard()
|
||||
}, [readClipboard])
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||
@ -55,31 +72,65 @@ const HomeWindow: FC = () => {
|
||||
const onCloseWindow = () => window.api.miniWindow.hide()
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const isEnterPressed = e.code === 'Enter'
|
||||
const isBackspacePressed = e.code === 'Backspace'
|
||||
|
||||
if (e.code === 'Escape') {
|
||||
setText('')
|
||||
setRoute('home')
|
||||
route === 'home' && onCloseWindow()
|
||||
// 使用非直接输入法时(例如中文、日文输入法),存在输入法键入过程
|
||||
// 键入过程不应有任何响应
|
||||
// 例子,中文输入法候选词过程使用`Enter`直接上屏字母,日文输入法候选词过程使用`Enter`输入假名
|
||||
// 输入法可以`Esc`终止候选词过程
|
||||
// 这两个例子的`Enter`和`Esc`快捷助手都不应该响应
|
||||
if (e.key === 'Process') {
|
||||
return
|
||||
}
|
||||
|
||||
if (isEnterPressed) {
|
||||
e.preventDefault()
|
||||
if (content) {
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
}
|
||||
|
||||
if (isBackspacePressed) {
|
||||
textChange(() => {
|
||||
if (text.length === 0) {
|
||||
clearClipboard()
|
||||
switch (e.code) {
|
||||
case 'Enter':
|
||||
{
|
||||
e.preventDefault()
|
||||
if (content) {
|
||||
if (route === 'home') {
|
||||
featureMenusRef.current?.useFeature()
|
||||
setText('')
|
||||
} else {
|
||||
// 目前文本框只在'chat'时可以继续输入,这里相当于 route === 'chat'
|
||||
setRoute('chat')
|
||||
onSendMessage().then()
|
||||
focusInput()
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'Backspace':
|
||||
{
|
||||
textChange(() => {
|
||||
if (text.length === 0) {
|
||||
clearClipboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
{
|
||||
if (route === 'home') {
|
||||
e.preventDefault()
|
||||
featureMenusRef.current?.prevFeature()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'ArrowDown':
|
||||
{
|
||||
if (route === 'home') {
|
||||
e.preventDefault()
|
||||
featureMenusRef.current?.nextFeature()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
{
|
||||
setText('')
|
||||
setRoute('home')
|
||||
route === 'home' && onCloseWindow()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +167,7 @@ const HomeWindow: FC = () => {
|
||||
setSelectedText('')
|
||||
}
|
||||
|
||||
// If the input is focused, the `Esc` callback will not be triggered here.
|
||||
useHotkeys('esc', () => {
|
||||
if (route === 'home') {
|
||||
onCloseWindow()
|
||||
@ -126,7 +178,7 @@ const HomeWindow: FC = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('show-mini-window', onReadClipboard)
|
||||
window.electron.ipcRenderer.on('show-mini-window', onWindowShow)
|
||||
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => {
|
||||
selectedText && setSelectedText(selectedText)
|
||||
action && setRoute(action)
|
||||
@ -137,7 +189,7 @@ const HomeWindow: FC = () => {
|
||||
window.electron.ipcRenderer.removeAllListeners('show-mini-window')
|
||||
window.electron.ipcRenderer.removeAllListeners('selection-action')
|
||||
}
|
||||
}, [onReadClipboard, onSendMessage, setRoute])
|
||||
}, [onWindowShow, onSendMessage, setRoute])
|
||||
|
||||
// 当路由为home时,初始化isFirstMessage为true
|
||||
useEffect(() => {
|
||||
@ -158,6 +210,7 @@ const HomeWindow: FC = () => {
|
||||
placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })}
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleChange={handleChange}
|
||||
ref={inputBarRef}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
</>
|
||||
@ -197,11 +250,12 @@ const HomeWindow: FC = () => {
|
||||
}
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleChange={handleChange}
|
||||
ref={inputBarRef}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
|
||||
<Main>
|
||||
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} text={content} />
|
||||
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} text={content} ref={featureMenusRef} />
|
||||
</Main>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { BulbOutlined, FileTextOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { BulbOutlined, EnterOutlined, FileTextOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { Col } from 'antd'
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { Dispatch, forwardRef, SetStateAction, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -11,7 +11,14 @@ interface FeatureMenusProps {
|
||||
onSendMessage: (prompt?: string) => void
|
||||
}
|
||||
|
||||
const FeatureMenus: FC<FeatureMenusProps> = ({ text, setRoute, onSendMessage }) => {
|
||||
export interface FeatureMenusRef {
|
||||
nextFeature: () => void
|
||||
prevFeature: () => void
|
||||
useFeature: () => void
|
||||
resetSelectedIndex: () => void
|
||||
}
|
||||
|
||||
const FeatureMenus = forwardRef<FeatureMenusRef, FeatureMenusProps>(({ text, setRoute, onSendMessage }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
@ -57,27 +64,20 @@ const FeatureMenus: FC<FeatureMenusProps> = ({ text, setRoute, onSendMessage })
|
||||
[onSendMessage, setRoute, t, text]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : features.length - 1))
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev < features.length - 1 ? prev + 1 : 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
features[selectedIndex].onClick?.()
|
||||
break
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
nextFeature() {
|
||||
setSelectedIndex((prev) => (prev < features.length - 1 ? prev + 1 : 0))
|
||||
},
|
||||
prevFeature() {
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : features.length - 1))
|
||||
},
|
||||
useFeature() {
|
||||
features[selectedIndex].onClick?.()
|
||||
},
|
||||
resetSelectedIndex() {
|
||||
setSelectedIndex(0)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [features, selectedIndex])
|
||||
}))
|
||||
|
||||
return (
|
||||
<FeatureList>
|
||||
@ -87,13 +87,15 @@ const FeatureMenus: FC<FeatureMenusProps> = ({ text, setRoute, onSendMessage })
|
||||
<FeatureItem onClick={feature.onClick} className={index === selectedIndex ? 'active' : ''}>
|
||||
<FeatureIcon>{feature.icon}</FeatureIcon>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
{index === selectedIndex && <EnterOutlined />}
|
||||
</FeatureItem>
|
||||
</Col>
|
||||
))}
|
||||
</FeatureListWrapper>
|
||||
</FeatureList>
|
||||
)
|
||||
}
|
||||
})
|
||||
FeatureMenus.displayName = 'FeatureMenus'
|
||||
|
||||
const FeatureList = styled(Scrollbar)`
|
||||
flex: 1;
|
||||
@ -109,7 +111,7 @@ const FeatureListWrapper = styled.div`
|
||||
|
||||
const FeatureItem = styled.div`
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transition: background-color 0s;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
@ -121,11 +123,11 @@ const FeatureItem = styled.div`
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-opacity);
|
||||
background: var(--color-background-mute);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-background-opacity);
|
||||
background: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
@ -136,6 +138,7 @@ const FeatureIcon = styled.div`
|
||||
const FeatureTitle = styled.h3`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
flex-basis: 100%;
|
||||
`
|
||||
|
||||
export default FeatureMenus
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import React, { FC } from 'react'
|
||||
import { InputRef } from 'rc-input/lib/interface'
|
||||
import React, { forwardRef, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface InputBarProps {
|
||||
@ -13,23 +14,31 @@ interface InputBarProps {
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown, handleChange }) => {
|
||||
const { generating } = useRuntime()
|
||||
return (
|
||||
<InputWrapper>
|
||||
<ModelAvatar model={model} size={30} />
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
bordered={false}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
disabled={generating}
|
||||
/>
|
||||
</InputWrapper>
|
||||
)
|
||||
}
|
||||
const InputBar = forwardRef<HTMLDivElement, InputBarProps>(
|
||||
({ text, model, placeholder, handleKeyDown, handleChange }, ref) => {
|
||||
const { generating } = useRuntime()
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
if (!generating) {
|
||||
setTimeout(() => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
return (
|
||||
<InputWrapper ref={ref}>
|
||||
<ModelAvatar model={model} size={30} />
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
bordered={false}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
disabled={generating}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</InputWrapper>
|
||||
)
|
||||
}
|
||||
)
|
||||
InputBar.displayName = 'InputBar'
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user