diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b15f092f..e7126a8f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 72204c44..b8015b7f 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "编辑话题名", diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 3d40b5c2..da8ad041 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -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 = ({ 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) } diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index b9a4dbfa..b812d99a 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -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 = ({ assistant: _assistant, activeTopic, setActiveTopic }) } }, + { + label: t('chat.topics.copy.title'), + key: 'copy', + icon: , + 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', diff --git a/src/renderer/src/services/EventService.ts b/src/renderer/src/services/EventService.ts index d8901b83..6d425556 100644 --- a/src/renderer/src/services/EventService.ts +++ b/src/renderer/src/services/EventService.ts @@ -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', diff --git a/src/renderer/src/utils/copy.ts b/src/renderer/src/utils/copy.ts new file mode 100644 index 00000000..5f0aeffc --- /dev/null +++ b/src/renderer/src/utils/copy.ts @@ -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) +} diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index 6c43e43c..e6463a18 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -329,7 +329,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject { @@ -344,7 +344,19 @@ export const captureScrollableDiv = async (divRef: React.RefObject) => { + return captureScrollableDiv(divRef).then((canvas) => { + if (canvas) { + return canvas.toDataURL('image/png') + } + return Promise.resolve(undefined) + }) +} +export const captureScrollableDivAsBlob = async (divRef: React.RefObject, func: BlobCallback) => { + await captureScrollableDiv(divRef).then((canvas) => { + canvas?.toBlob(func, 'image/png') + }) +} export function hasPath(url: string): boolean { try { const parsedUrl = new URL(url)