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