feat: add "Copy as" options to topics right click menu (#2095)
* feat: Add copy topic as image and Markdown functionality * add translation
This commit is contained in:
parent
1c163c55b8
commit
cf2d7ba8b4
@ -126,6 +126,9 @@
|
||||
"suggestions.title": "Suggested Questions",
|
||||
"thinking": "Thinking",
|
||||
"topics.auto_rename": "Auto Rename",
|
||||
"topics.copy.title": "Copy as",
|
||||
"topics.copy.image": "Image",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
"topics.edit.title": "Edit Name",
|
||||
|
||||
@ -128,6 +128,9 @@
|
||||
"suggestions.title": "建议的问题",
|
||||
"thinking": "思考中",
|
||||
"topics.auto_rename": "生成话题名",
|
||||
"topics.copy.title": "复制为",
|
||||
"topics.copy.image": "图片",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
"topics.edit.title": "编辑话题名",
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from '@renderer/services/MessagesService'
|
||||
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { captureScrollableDiv, runAsyncFunction } from '@renderer/utils'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, runAsyncFunction } from '@renderer/utils'
|
||||
import { t } from 'i18next'
|
||||
import { flatten, last, take } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -170,8 +170,15 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
_topic && updateTopic({ ..._topic, name: defaultTopic.name, messages: [] })
|
||||
TopicManager.clearTopicMessages(topic.id)
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
||||
if (blob) {
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||
}
|
||||
})
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
||||
const imageData = await captureScrollableDiv(containerRef)
|
||||
const imageData = await captureScrollableDivAsDataURL(containerRef)
|
||||
if (imageData) {
|
||||
window.api.file.saveImage(topic.name, imageData)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
ClearOutlined,
|
||||
CloseOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FolderOutlined,
|
||||
@ -21,6 +22,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||
import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export'
|
||||
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
@ -189,6 +191,23 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.copy.title'),
|
||||
key: 'copy',
|
||||
icon: <CopyOutlined />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.topics.copy.image'),
|
||||
key: 'img',
|
||||
onClick: () => EventEmitter.emit(EVENT_NAMES.COPY_TOPIC_IMAGE, topic)
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.copy.md'),
|
||||
key: 'md',
|
||||
onClick: () => copyTopicAsMarkdown(topic)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.title'),
|
||||
key: 'export',
|
||||
|
||||
@ -19,6 +19,7 @@ export const EVENT_NAMES = {
|
||||
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR',
|
||||
NEW_CONTEXT: 'NEW_CONTEXT',
|
||||
NEW_BRANCH: 'NEW_BRANCH',
|
||||
COPY_TOPIC_IMAGE: 'COPY_TOPIC_IMAGE',
|
||||
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
||||
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
||||
|
||||
8
src/renderer/src/utils/copy.ts
Normal file
8
src/renderer/src/utils/copy.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Topic } from '@renderer/types'
|
||||
|
||||
import { topicToMarkdown } from './export'
|
||||
|
||||
export const copyTopicAsMarkdown = async (topic: Topic) => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
}
|
||||
@ -329,7 +329,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
|
||||
div.style.overflow = originalStyle.overflow
|
||||
div.style.position = originalStyle.position
|
||||
|
||||
const imageData = canvas.toDataURL('image/png')
|
||||
const imageData = canvas
|
||||
|
||||
// Restore original scroll position
|
||||
setTimeout(() => {
|
||||
@ -344,7 +344,19 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
|
||||
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
|
||||
export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTMLDivElement>) => {
|
||||
return captureScrollableDiv(divRef).then((canvas) => {
|
||||
if (canvas) {
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
})
|
||||
}
|
||||
export const captureScrollableDivAsBlob = async (divRef: React.RefObject<HTMLDivElement>, func: BlobCallback) => {
|
||||
await captureScrollableDiv(divRef).then((canvas) => {
|
||||
canvas?.toBlob(func, 'image/png')
|
||||
})
|
||||
}
|
||||
export function hasPath(url: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(url)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user