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",
|
"suggestions.title": "Suggested Questions",
|
||||||
"thinking": "Thinking",
|
"thinking": "Thinking",
|
||||||
"topics.auto_rename": "Auto Rename",
|
"topics.auto_rename": "Auto Rename",
|
||||||
|
"topics.copy.title": "Copy as",
|
||||||
|
"topics.copy.image": "Image",
|
||||||
|
"topics.copy.md": "Markdown",
|
||||||
"topics.clear.title": "Clear Messages",
|
"topics.clear.title": "Clear Messages",
|
||||||
"topics.edit.placeholder": "Enter new name",
|
"topics.edit.placeholder": "Enter new name",
|
||||||
"topics.edit.title": "Edit Name",
|
"topics.edit.title": "Edit Name",
|
||||||
|
|||||||
@ -128,6 +128,9 @@
|
|||||||
"suggestions.title": "建议的问题",
|
"suggestions.title": "建议的问题",
|
||||||
"thinking": "思考中",
|
"thinking": "思考中",
|
||||||
"topics.auto_rename": "生成话题名",
|
"topics.auto_rename": "生成话题名",
|
||||||
|
"topics.copy.title": "复制为",
|
||||||
|
"topics.copy.image": "图片",
|
||||||
|
"topics.copy.md": "Markdown",
|
||||||
"topics.clear.title": "清空消息",
|
"topics.clear.title": "清空消息",
|
||||||
"topics.edit.placeholder": "输入新名称",
|
"topics.edit.placeholder": "输入新名称",
|
||||||
"topics.edit.title": "编辑话题名",
|
"topics.edit.title": "编辑话题名",
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
} from '@renderer/services/MessagesService'
|
} from '@renderer/services/MessagesService'
|
||||||
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
||||||
import { Assistant, Message, Topic } from '@renderer/types'
|
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 { t } from 'i18next'
|
||||||
import { flatten, last, take } from 'lodash'
|
import { flatten, last, take } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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: [] })
|
_topic && updateTopic({ ..._topic, name: defaultTopic.name, messages: [] })
|
||||||
TopicManager.clearTopicMessages(topic.id)
|
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 () => {
|
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
||||||
const imageData = await captureScrollableDiv(containerRef)
|
const imageData = await captureScrollableDivAsDataURL(containerRef)
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
window.api.file.saveImage(topic.name, imageData)
|
window.api.file.saveImage(topic.name, imageData)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
@ -21,6 +22,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||||
import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export'
|
import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export'
|
||||||
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
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'),
|
label: t('chat.topics.export.title'),
|
||||||
key: 'export',
|
key: 'export',
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const EVENT_NAMES = {
|
|||||||
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR',
|
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR',
|
||||||
NEW_CONTEXT: 'NEW_CONTEXT',
|
NEW_CONTEXT: 'NEW_CONTEXT',
|
||||||
NEW_BRANCH: 'NEW_BRANCH',
|
NEW_BRANCH: 'NEW_BRANCH',
|
||||||
|
COPY_TOPIC_IMAGE: 'COPY_TOPIC_IMAGE',
|
||||||
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
||||||
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
||||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
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.overflow = originalStyle.overflow
|
||||||
div.style.position = originalStyle.position
|
div.style.position = originalStyle.position
|
||||||
|
|
||||||
const imageData = canvas.toDataURL('image/png')
|
const imageData = canvas
|
||||||
|
|
||||||
// Restore original scroll position
|
// Restore original scroll position
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -344,7 +344,19 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
|
|||||||
|
|
||||||
return Promise.resolve(undefined)
|
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 {
|
export function hasPath(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url)
|
const parsedUrl = new URL(url)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user