feat: added copy last message feature and translations

This commit is contained in:
kangfenmao 2025-01-20 11:07:08 +08:00
parent c510f5dcce
commit 749353f460
11 changed files with 65 additions and 36 deletions

View File

@ -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"
} }
} }
} }

View File

@ -652,7 +652,8 @@
"footer": { "footer": {
"esc": "ESC キーを押して{{action}}", "esc": "ESC キーを押して{{action}}",
"esc_close": "ウィンドウを閉じる", "esc_close": "ウィンドウを閉じる",
"esc_back": "戻る" "esc_back": "戻る",
"copy_last_message": "C キーを押してコピー"
} }
} }
} }

View File

@ -666,7 +666,8 @@
"footer": { "footer": {
"esc": "Нажмите ESC {{action}}", "esc": "Нажмите ESC {{action}}",
"esc_close": "закрытия окна", "esc_close": "закрытия окна",
"esc_back": "возвращения" "esc_back": "возвращения",
"copy_last_message": "Нажмите C для копирования"
} }
} }
} }

View File

@ -655,7 +655,8 @@
"footer": { "footer": {
"esc": "按 ESC {{action}}", "esc": "按 ESC {{action}}",
"esc_close": "关闭窗口", "esc_close": "关闭窗口",
"esc_back": "返回" "esc_back": "返回",
"copy_last_message": "按 C 键复制"
} }
} }
} }

View File

@ -654,7 +654,8 @@
"footer": { "footer": {
"esc": "按 ESC {{action}}", "esc": "按 ESC {{action}}",
"esc_close": "關閉窗口", "esc_close": "關閉窗口",
"esc_back": "返回" "esc_back": "返回",
"copy_last_message": "按 C 鍵複製"
} }
} }
} }

View File

@ -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

View File

@ -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)

View File

@ -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')
onSendMessage()
setTimeout(() => setText(''), 100)
} }
setRoute('chat')
onSendMessage()
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

View File

@ -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,29 +20,35 @@ const FeatureMenus: FC<FeatureMenusProps> = ({ setRoute, onSendMessage }) => {
title: t('miniwindow.feature.chat'), title: t('miniwindow.feature.chat'),
active: true, active: true,
onClick: () => { onClick: () => {
setRoute('chat') if (text) {
onSendMessage() setRoute('chat')
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: () => {
setRoute('summary') if (text) {
onSendMessage(t('prompts.summarize')) setRoute('summary')
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: () => {
setRoute('explanation') if (text) {
onSendMessage(t('prompts.explanation')) setRoute('explanation')
onSendMessage(t('prompts.explanation'))
}
} }
} }
] ]

View File

@ -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">
{t('miniwindow.footer.esc', { <Tag bordered={false} icon={<LoginOutlined />}>
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back') {t('miniwindow.footer.esc', {
})} 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;
` `

View File

@ -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>