feat: 添加翻译历史功能
This commit is contained in:
parent
b31b1c7908
commit
02930a2793
@ -1,4 +1,4 @@
|
|||||||
import { FileType, KnowledgeItem, Topic } from '@renderer/types'
|
import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types'
|
||||||
import { Dexie, type EntityTable } from 'dexie'
|
import { Dexie, type EntityTable } from 'dexie'
|
||||||
|
|
||||||
// Database declaration (move this to its own module also)
|
// Database declaration (move this to its own module also)
|
||||||
@ -7,6 +7,7 @@ export const db = new Dexie('CherryStudio') as Dexie & {
|
|||||||
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
||||||
settings: EntityTable<{ id: string; value: any }, 'id'>
|
settings: EntityTable<{ id: string; value: any }, 'id'>
|
||||||
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
|
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
|
||||||
|
translate_history: EntityTable<TranslateHistory, 'id'>
|
||||||
}
|
}
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
@ -26,4 +27,12 @@ db.version(3).stores({
|
|||||||
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
|
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
db.version(4).stores({
|
||||||
|
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||||
|
topics: '&id, messages',
|
||||||
|
settings: '&id, value',
|
||||||
|
knowledge_notes: '&id, baseId, type, content, created_at, updated_at',
|
||||||
|
translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt'
|
||||||
|
})
|
||||||
|
|
||||||
export default db
|
export default db
|
||||||
|
|||||||
@ -786,7 +786,14 @@
|
|||||||
"input.placeholder": "Enter text to translate",
|
"input.placeholder": "Enter text to translate",
|
||||||
"output.placeholder": "Translation",
|
"output.placeholder": "Translation",
|
||||||
"processing": "Translation in progress...",
|
"processing": "Translation in progress...",
|
||||||
"title": "Translation"
|
"title": "Translation",
|
||||||
|
"history": {
|
||||||
|
"title": "Translation History",
|
||||||
|
"empty": "No translation history",
|
||||||
|
"clear": "Clear History",
|
||||||
|
"delete": "Delete",
|
||||||
|
"clear_description": "Clear history will delete all translation history, continue?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
|
|||||||
@ -790,7 +790,14 @@
|
|||||||
"input.placeholder": "输入文本进行翻译",
|
"input.placeholder": "输入文本进行翻译",
|
||||||
"output.placeholder": "翻译",
|
"output.placeholder": "翻译",
|
||||||
"processing": "翻译中...",
|
"processing": "翻译中...",
|
||||||
"title": "翻译"
|
"title": "翻译",
|
||||||
|
"history": {
|
||||||
|
"title": "翻译历史",
|
||||||
|
"empty": "暂无翻译历史",
|
||||||
|
"clear": "清空历史",
|
||||||
|
"delete": "删除",
|
||||||
|
"clear_description": "清空历史将删除所有翻译历史记录,是否继续?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
|
|||||||
@ -1,4 +1,12 @@
|
|||||||
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
|
import {
|
||||||
|
CheckOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
SwapOutlined,
|
||||||
|
WarningOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
@ -7,10 +15,12 @@ import db from '@renderer/databases'
|
|||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import { Assistant, Message } from '@renderer/types'
|
import { Assistant, Message, TranslateHistory } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { Button, Flex, Select, Space } from 'antd'
|
import { Button, Dropdown, Empty, Flex, Popconfirm, Select, Space } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -29,13 +39,42 @@ const TranslatePage: FC = () => {
|
|||||||
const { translateModel } = useDefaultModel()
|
const { translateModel } = useDefaultModel()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const textAreaRef = useRef<TextAreaRef>(null)
|
const textAreaRef = useRef<TextAreaRef>(null)
|
||||||
|
|
||||||
|
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_result = result
|
_result = result
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
|
const saveTranslateHistory = async (
|
||||||
|
sourceText: string,
|
||||||
|
targetText: string,
|
||||||
|
sourceLanguage: string,
|
||||||
|
targetLanguage: string
|
||||||
|
) => {
|
||||||
|
const history: TranslateHistory = {
|
||||||
|
id: uuid(),
|
||||||
|
sourceText,
|
||||||
|
targetText,
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
console.log('🌟TEO🌟 ~ saveTranslateHistory ~ history:', history)
|
||||||
|
await db.translate_history.add(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHistory = async (id: string) => {
|
||||||
|
db.translate_history.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearHistory = async () => {
|
||||||
|
db.translate_history.clear()
|
||||||
|
}
|
||||||
|
|
||||||
const onTranslate = async () => {
|
const onTranslate = async () => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
return
|
return
|
||||||
@ -64,7 +103,17 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
await fetchTranslate({ message, assistant, onResponse: (text) => setResult(text) })
|
let translatedText = ''
|
||||||
|
await fetchTranslate({
|
||||||
|
message,
|
||||||
|
assistant,
|
||||||
|
onResponse: (text) => {
|
||||||
|
translatedText = text
|
||||||
|
setResult(text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await saveTranslateHistory(text, translatedText, 'any', targetLanguage)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +123,12 @@ const TranslatePage: FC = () => {
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onHistoryItemClick = (history: TranslateHistory) => {
|
||||||
|
setText(history.sourceText)
|
||||||
|
setResult(history.targetText)
|
||||||
|
setTargetLanguage(history.targetLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isEmpty(text) && setResult('')
|
isEmpty(text) && setResult('')
|
||||||
}, [text])
|
}, [text])
|
||||||
@ -113,9 +168,69 @@ const TranslatePage: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Container id="translate-page">
|
<Container id="translate-page">
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>
|
||||||
|
{t('translate.title')}
|
||||||
|
<Button
|
||||||
|
className="nodrag"
|
||||||
|
color="default"
|
||||||
|
variant={historyDrawerVisible ? 'filled' : 'text'}
|
||||||
|
type="text"
|
||||||
|
icon={<HistoryOutlined />}
|
||||||
|
onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)}
|
||||||
|
/>
|
||||||
|
</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer id="content-container" ref={contentContainerRef}>
|
<ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}>
|
||||||
|
<HistoryContainner $historyDrawerVisible={historyDrawerVisible}>
|
||||||
|
<OperationBar>
|
||||||
|
<span style={{ fontSize: 16 }}>{t('translate.history.title')}</span>
|
||||||
|
{translateHistory?.length && (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('translate.history.clear')}
|
||||||
|
description={t('translate.history.clear_description')}
|
||||||
|
onConfirm={clearHistory}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No">
|
||||||
|
<Button type="text" size="small" danger icon={<DeleteOutlined />}>
|
||||||
|
{t('translate.history.clear')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</OperationBar>
|
||||||
|
{translateHistory && translateHistory.length ? (
|
||||||
|
<HistoryList>
|
||||||
|
{translateHistory.map((item) => (
|
||||||
|
<Dropdown
|
||||||
|
key={item.id}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: t('translate.history.delete'),
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
danger: true,
|
||||||
|
onClick: () => deleteHistory(item.id)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}>
|
||||||
|
<HistoryListItem onClick={() => onHistoryItemClick(item)}>
|
||||||
|
<Flex justify="space-between" vertical gap={4} style={{ width: '100%' }}>
|
||||||
|
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
|
||||||
|
<HistoryListItemTitle>{item.targetText}</HistoryListItemTitle>
|
||||||
|
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
|
||||||
|
</Flex>
|
||||||
|
</HistoryListItem>
|
||||||
|
</Dropdown>
|
||||||
|
))}
|
||||||
|
</HistoryList>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" align="center" style={{ flex: 1 }}>
|
||||||
|
<Empty description={t('translate.history.empty')} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</HistoryContainner>
|
||||||
|
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<OperationBar>
|
<OperationBar>
|
||||||
<Flex align="center" gap={20}>
|
<Flex align="center" gap={20}>
|
||||||
@ -153,7 +268,7 @@ const TranslatePage: FC = () => {
|
|||||||
</InputContainer>
|
</InputContainer>
|
||||||
|
|
||||||
<Flex justify="center" align="center">
|
<Flex justify="center" align="center">
|
||||||
<SwapOutlined />
|
<SwapOutlined style={{ color: 'var(--color-text-2)' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<OutputContainer>
|
<OutputContainer>
|
||||||
@ -195,12 +310,13 @@ const Container = styled.div`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div<{ $historyDrawerVisible: boolean }>`
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 40px 1fr;
|
grid-template-columns: auto 1fr 40px 1fr;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
const InputContainer = styled.div`
|
const InputContainer = styled.div`
|
||||||
@ -257,4 +373,83 @@ const TranslateButton = styled(Button)``
|
|||||||
|
|
||||||
const CopyButton = styled(Button)``
|
const CopyButton = styled(Button)``
|
||||||
|
|
||||||
|
const HistoryContainner = styled.div<{ $historyDrawerVisible: boolean }>`
|
||||||
|
width: ${({ $historyDrawerVisible }) => ($historyDrawerVisible ? '300px' : '0')};
|
||||||
|
height: calc(100vh - var(--navbar-height) - 40px);
|
||||||
|
transition:
|
||||||
|
width 0.2s,
|
||||||
|
opacity 0.2s;
|
||||||
|
border: 1px solid var(--color-border-soft);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 2px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
${({ $historyDrawerVisible }) =>
|
||||||
|
!$historyDrawerVisible &&
|
||||||
|
`
|
||||||
|
border: none;
|
||||||
|
margin-right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryList = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItem = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: var(--list-item-border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
button {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
border-bottom: 1px dashed var(--color-border-soft);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItemTitle = styled.div`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItemDate = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
`
|
||||||
|
|
||||||
export default TranslatePage
|
export default TranslatePage
|
||||||
|
|||||||
@ -271,4 +271,13 @@ export type GenerateImageParams = {
|
|||||||
promptEnhancement?: boolean
|
promptEnhancement?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TranslateHistory {
|
||||||
|
id: string
|
||||||
|
sourceText: string
|
||||||
|
targetText: string
|
||||||
|
sourceLanguage: string
|
||||||
|
targetLanguage: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user