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:
落子 2025-02-21 13:59:34 +08:00 committed by GitHub
parent 1c163c55b8
commit cf2d7ba8b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 57 additions and 4 deletions

View File

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

View File

@ -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": "编辑话题名",

View File

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

View File

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

View File

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

View 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)
}

View File

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