feat: added copy last message feature and translations
This commit is contained in:
parent
c510f5dcce
commit
749353f460
@ -667,7 +667,8 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"esc": "Press ESC {{action}}",
|
"esc": "Press ESC {{action}}",
|
||||||
"esc_close": "close the window",
|
"esc_close": "close the window",
|
||||||
"esc_back": "back"
|
"esc_back": "back",
|
||||||
|
"copy_last_message": "Press C to copy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -652,7 +652,8 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"esc": "ESC キーを押して{{action}}",
|
"esc": "ESC キーを押して{{action}}",
|
||||||
"esc_close": "ウィンドウを閉じる",
|
"esc_close": "ウィンドウを閉じる",
|
||||||
"esc_back": "戻る"
|
"esc_back": "戻る",
|
||||||
|
"copy_last_message": "C キーを押してコピー"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -666,7 +666,8 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"esc": "Нажмите ESC {{action}}",
|
"esc": "Нажмите ESC {{action}}",
|
||||||
"esc_close": "закрытия окна",
|
"esc_close": "закрытия окна",
|
||||||
"esc_back": "возвращения"
|
"esc_back": "возвращения",
|
||||||
|
"copy_last_message": "Нажмите C для копирования"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -655,7 +655,8 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"esc": "按 ESC {{action}}",
|
"esc": "按 ESC {{action}}",
|
||||||
"esc_close": "关闭窗口",
|
"esc_close": "关闭窗口",
|
||||||
"esc_back": "返回"
|
"esc_back": "返回",
|
||||||
|
"copy_last_message": "按 C 键复制"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -654,7 +654,8 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"esc": "按 ESC {{action}}",
|
"esc": "按 ESC {{action}}",
|
||||||
"esc_close": "關閉窗口",
|
"esc_close": "關閉窗口",
|
||||||
"esc_back": "返回"
|
"esc_back": "返回",
|
||||||
|
"copy_last_message": "按 C 鍵複製"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,7 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
|
setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
|
||||||
state.language = action.payload
|
state.language = action.payload
|
||||||
|
window.electron.ipcRenderer.send('miniwindow-reload')
|
||||||
},
|
},
|
||||||
setProxyMode: (state, action: PayloadAction<'system' | 'custom' | 'none'>) => {
|
setProxyMode: (state, action: PayloadAction<'system' | 'custom' | 'none'>) => {
|
||||||
state.proxyMode = action.payload
|
state.proxyMode = action.payload
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getAssistantMessage } from '@renderer/services/MessagesService'
|
import { getAssistantMessage } from '@renderer/services/MessagesService'
|
||||||
import { Assistant, Message } from '@renderer/types'
|
import { Assistant, Message } from '@renderer/types'
|
||||||
import { last } from 'lodash'
|
import { last } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
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'
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [assistant.id, onSendMessage])
|
}, [assistant.id, onSendMessage])
|
||||||
|
|
||||||
useShortcut('copy_last_message', () => {
|
useHotkeys('c', () => {
|
||||||
const lastMessage = last(messages)
|
const lastMessage = last(messages)
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
navigator.clipboard.writeText(lastMessage.content)
|
navigator.clipboard.writeText(lastMessage.content)
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import { EventEmitter } from '@renderer/services/EventService'
|
|||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Divider } from 'antd'
|
import { Divider } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
import { isEmpty } from 'lodash'
|
||||||
|
import { FC, useCallback, useEffect, 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'
|
||||||
@ -28,11 +29,10 @@ const HomeWindow: FC = () => {
|
|||||||
const { defaultModel: model } = useDefaultModel()
|
const { defaultModel: model } = useDefaultModel()
|
||||||
const { language } = useSettings()
|
const { language } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const textRef = useRef(text)
|
|
||||||
|
|
||||||
const referenceText = selectedText || clipboardText || text
|
const referenceText = selectedText || clipboardText || text
|
||||||
|
|
||||||
textRef.current = referenceText === text ? text : `${referenceText}\n\n${text}`
|
const content = (referenceText === text ? text : `${referenceText}\n\n${text}`).trim()
|
||||||
|
|
||||||
const onReadClipboard = useCallback(async () => {
|
const onReadClipboard = useCallback(async () => {
|
||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
@ -59,23 +59,25 @@ const HomeWindow: FC = () => {
|
|||||||
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (textRef.current.trim() === '') {
|
if (content) {
|
||||||
return
|
|
||||||
}
|
|
||||||
setRoute('chat')
|
setRoute('chat')
|
||||||
onSendMessage()
|
onSendMessage()
|
||||||
setTimeout(() => setText(''), 100)
|
setTimeout(() => setText(''), 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
const onSendMessage = useCallback(
|
||||||
async (prompt?: string) => {
|
async (prompt?: string) => {
|
||||||
const text = textRef.current.trim()
|
if (isEmpty(content)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const message = {
|
const message = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: prompt ? `${prompt}\n\n${text}` : text,
|
content: prompt ? `${prompt}\n\n${content}` : content,
|
||||||
assistantId: defaultAssistant.id,
|
assistantId: defaultAssistant.id,
|
||||||
topicId: defaultAssistant.topics[0].id || uuid(),
|
topicId: defaultAssistant.topics[0].id || uuid(),
|
||||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
@ -85,7 +87,7 @@ const HomeWindow: FC = () => {
|
|||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
[defaultAssistant]
|
[content, defaultAssistant.id, defaultAssistant.topics]
|
||||||
)
|
)
|
||||||
|
|
||||||
const clearClipboard = () => {
|
const clearClipboard = () => {
|
||||||
@ -104,12 +106,8 @@ const HomeWindow: FC = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.ipcRenderer.on('show-mini-window', () => {
|
window.electron.ipcRenderer.on('show-mini-window', onReadClipboard)
|
||||||
onReadClipboard()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => {
|
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => {
|
||||||
console.debug('[HomeWindow] selection-action', action, selectedText)
|
|
||||||
selectedText && setSelectedText(selectedText)
|
selectedText && setSelectedText(selectedText)
|
||||||
action && setRoute(action)
|
action && setRoute(action)
|
||||||
action === 'chat' && onSendMessage()
|
action === 'chat' && onSendMessage()
|
||||||
@ -176,7 +174,7 @@ const HomeWindow: FC = () => {
|
|||||||
<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} />
|
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} text={content} />
|
||||||
</Main>
|
</Main>
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface FeatureMenusProps {
|
interface FeatureMenusProps {
|
||||||
|
text: string
|
||||||
setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>>
|
setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>>
|
||||||
onSendMessage: (prompt?: string) => void
|
onSendMessage: (prompt?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureMenus: FC<FeatureMenusProps> = ({ setRoute, onSendMessage }) => {
|
const FeatureMenus: FC<FeatureMenusProps> = ({ text, setRoute, onSendMessage }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
@ -19,31 +20,37 @@ const FeatureMenus: FC<FeatureMenusProps> = ({ setRoute, onSendMessage }) => {
|
|||||||
title: t('miniwindow.feature.chat'),
|
title: t('miniwindow.feature.chat'),
|
||||||
active: true,
|
active: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
if (text) {
|
||||||
setRoute('chat')
|
setRoute('chat')
|
||||||
onSendMessage()
|
onSendMessage()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TranslationOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
icon: <TranslationOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||||
title: t('miniwindow.feature.translate'),
|
title: t('miniwindow.feature.translate'),
|
||||||
onClick: () => setRoute('translate')
|
onClick: () => text && setRoute('translate')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FileTextOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
icon: <FileTextOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||||
title: t('miniwindow.feature.summary'),
|
title: t('miniwindow.feature.summary'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
if (text) {
|
||||||
setRoute('summary')
|
setRoute('summary')
|
||||||
onSendMessage(t('prompts.summarize'))
|
onSendMessage(t('prompts.summarize'))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BulbOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
icon: <BulbOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||||
title: t('miniwindow.feature.explanation'),
|
title: t('miniwindow.feature.explanation'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
if (text) {
|
||||||
setRoute('explanation')
|
setRoute('explanation')
|
||||||
onSendMessage(t('prompts.explanation'))
|
onSendMessage(t('prompts.explanation'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { CopyOutlined, LoginOutlined } from '@ant-design/icons'
|
||||||
|
import { Tag } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -13,9 +15,16 @@ const Footer: FC<FooterProps> = ({ route, onExit }) => {
|
|||||||
return (
|
return (
|
||||||
<WindowFooter onClick={() => onExit()}>
|
<WindowFooter onClick={() => onExit()}>
|
||||||
<FooterText className="nodrag">
|
<FooterText className="nodrag">
|
||||||
|
<Tag bordered={false} icon={<LoginOutlined />}>
|
||||||
{t('miniwindow.footer.esc', {
|
{t('miniwindow.footer.esc', {
|
||||||
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
||||||
})}
|
})}
|
||||||
|
</Tag>
|
||||||
|
{route !== 'home' && (
|
||||||
|
<Tag bordered={false} icon={<CopyOutlined />}>
|
||||||
|
{t('miniwindow.footer.copy_last_message')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</FooterText>
|
</FooterText>
|
||||||
</WindowFooter>
|
</WindowFooter>
|
||||||
)
|
)
|
||||||
@ -29,7 +38,11 @@ const WindowFooter = styled.div`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FooterText = styled.span`
|
const FooterText = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`
|
`
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { runAsyncFunction, uuid } from '@renderer/utils'
|
|||||||
import { Select, Space } from 'antd'
|
import { Select, Space } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
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'
|
||||||
|
|
||||||
@ -71,6 +72,10 @@ const Translate: FC<Props> = ({ text }) => {
|
|||||||
translate()
|
translate()
|
||||||
}, [translate])
|
}, [translate])
|
||||||
|
|
||||||
|
useHotkeys('c', () => {
|
||||||
|
navigator.clipboard.writeText(result)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user