From acb2ea30fbc3248414edd86e7af4907af1629ec5 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 21 Feb 2025 16:27:07 +0800 Subject: [PATCH] feat: add export function to message --- src/renderer/src/assets/styles/index.scss | 1 - src/renderer/src/config/providers.ts | 2 +- src/renderer/src/i18n/locales/en-us.json | 6 +- src/renderer/src/i18n/locales/ja-jp.json | 6 +- src/renderer/src/i18n/locales/ru-ru.json | 6 +- src/renderer/src/i18n/locales/zh-cn.json | 6 +- src/renderer/src/i18n/locales/zh-tw.json | 6 +- .../src/pages/home/Messages/Message.tsx | 2 + .../src/pages/home/Messages/MessageGroup.tsx | 46 ++++++++++--- .../home/Messages/MessageGroupMenuBar.tsx | 4 +- .../pages/home/Messages/MessageMenubar.tsx | 67 ++++++++++++++++++- .../src/pages/home/Messages/Messages.tsx | 1 - .../src/pages/home/Tabs/TopicsTab.tsx | 11 +-- src/renderer/src/services/MessagesService.ts | 25 +++++++ src/renderer/src/utils/export.ts | 25 ++++--- src/renderer/src/utils/index.ts | 3 + 16 files changed, 172 insertions(+), 45 deletions(-) diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 3d2c38a8..02b631db 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -257,7 +257,6 @@ body, } } .group-menu-bar { - margin-left: 0; background-color: var(--color-background); } code { diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index a68d81ae..ace43994 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -337,7 +337,7 @@ export const PROVIDER_CONFIG = { }, websites: { official: 'https://console.volcengine.com/ark/', - apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey', + apiKey: 'https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=DB4II4FC', docs: 'https://www.volcengine.com/docs/82379/1182403', models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint' } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 21435244..37e59515 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -129,9 +129,9 @@ "thinking": "Thinking", "topics.auto_rename": "Auto Rename", "topics.clear.title": "Clear Messages", - "topics.copy.image": "Image", - "topics.copy.md": "Markdown", - "topics.copy.title": "Copy as", + "topics.copy.image": "Copy as image", + "topics.copy.md": "Copy as markdown", + "topics.copy.title": "Copy", "topics.delete.shortcut": "Hold {{key}} to delete directly", "topics.edit.placeholder": "Enter new name", "topics.edit.title": "Edit Name", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 9a852f78..0049fe1b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -129,9 +129,9 @@ "thinking": "思考中...", "topics.auto_rename": "自動リネーム", "topics.clear.title": "メッセージをクリア", - "topics.copy.image": "画像", - "topics.copy.md": "Markdown", - "topics.copy.title": "複製", + "topics.copy.image": "画像としてコピー", + "topics.copy.md": "Markdownとしてコピー", + "topics.copy.title": "コピー", "topics.delete.shortcut": "{{key}}キーを押しながらで直接削除", "topics.edit.placeholder": "新しい名前を入力", "topics.edit.title": "名前を編集", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f76ce794..084ec4f0 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -129,9 +129,9 @@ "thinking": "Мыслим", "topics.auto_rename": "Автопереименование", "topics.clear.title": "Очистить сообщения", - "topics.copy.image": "Изображение", - "topics.copy.md": "Markdown", - "topics.copy.title": "Скопировать как", + "topics.copy.image": "Скопировать как изображение", + "topics.copy.md": "Скопировать как Markdown", + "topics.copy.title": "Скопировать", "topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления", "topics.edit.placeholder": "Введите новый заголовок", "topics.edit.title": "Редактировать заголовок", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5a6f0ab9..32b4b5c5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -129,9 +129,9 @@ "thinking": "思考中", "topics.auto_rename": "生成话题名", "topics.clear.title": "清空消息", - "topics.copy.image": "图片", - "topics.copy.md": "Markdown", - "topics.copy.title": "复制为", + "topics.copy.image": "复制为图片", + "topics.copy.md": "复制为 Markdown", + "topics.copy.title": "复制", "topics.delete.shortcut": "按住 {{key}} 可直接删除", "topics.edit.placeholder": "输入新名称", "topics.edit.title": "编辑话题名", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b57bc825..4aa813eb 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -129,9 +129,9 @@ "thinking": "思考中", "topics.auto_rename": "自動重新命名", "topics.clear.title": "清空消息", - "topics.copy.image": "圖片", - "topics.copy.md": "Markdown", - "topics.copy.title": "複製為", + "topics.copy.image": "複製為圖片", + "topics.copy.md": "複製為 Markdown", + "topics.copy.title": "複製", "topics.delete.shortcut": "按住 {{key}} 可直接刪除", "topics.edit.placeholder": "輸入新名稱", "topics.edit.title": "編輯名稱", diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 6235996c..f5971e96 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -208,6 +208,7 @@ const MessageItem: FC = ({ isLastMessage={isLastMessage} isAssistantMessage={isAssistantMessage} isGrouped={isGrouped} + messageContainerRef={messageContainerRef} setModel={setModel} onEditMessage={onEditMessage} onDeleteMessage={onDeleteMessage} @@ -225,6 +226,7 @@ const MessageContainer = styled.div` flex-direction: column; position: relative; transition: background-color 0.3s ease; + padding: 0 20px; &.message-highlight { background-color: var(--color-primary-mute); } diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 648130c1..090cffbe 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -2,6 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import { useSettings } from '@renderer/hooks/useSettings' import { MultiModelMessageStyle } from '@renderer/store/settings' import { Message, Topic } from '@renderer/types' +import { classNames } from '@renderer/utils' import { Popover } from 'antd' import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -40,6 +41,7 @@ const MessageGroup: FC = ({ const isGrouped = messageLength > 1 const isHorizontal = multiModelMessageStyle === 'horizontal' + const isGrid = multiModelMessageStyle === 'grid' const onDelete = useCallback(async () => { window.modal.confirm({ @@ -62,10 +64,17 @@ const MessageGroup: FC = ({ }, [messageLength]) return ( - - + + {messages.map((message, index) => { - const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped + const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped if (isGridGroupMessage) { return ( = ({ const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>` padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')}; + &.group-container.horizontal, + &.group-container.grid { + padding: 0 20px; + .message { + padding: 0; + } + .group-menu-bar { + margin-left: 0; + margin-right: 0; + } + } ` const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>` width: 100%; display: grid; + gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')}; + overflow-y: auto; grid-template-columns: repeat( ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)}, minmax(550px, 1fr) ); - gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')}; @media (max-width: 800px) { grid-template-columns: repeat( ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)}, minmax(400px, 1fr) ); } - overflow-y: auto; + ${({ $layout }) => + $layout === 'horizontal' && + css` + margin-top: 15px; + `} ${({ $gridColumns, $layout, $count }) => $layout === 'grid' && css` + margin-top: 15px; grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr)); grid-template-rows: auto; gap: 16px; - margin-top: 20px; `} ` @@ -220,11 +245,11 @@ const MessageWrapper = styled(Scrollbar)` return '' }} - ${({ $layout, $isInPopover, $isGrouped }) => - $layout === 'grid' && $isGrouped + ${({ $layout, $isInPopover, $isGrouped }) => { + return $layout === 'grid' && $isGrouped ? css` max-height: ${$isInPopover ? '50vh' : '300px'}; - overflow-y: auto; + overflow-y: ${$isInPopover ? 'auto' : 'hidden'}; border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'}; padding: 10px; border-radius: 6px; @@ -233,7 +258,8 @@ const MessageWrapper = styled(Scrollbar)` : css` overflow-y: auto; border-radius: 6px; - `} + ` + }} ` export default memo(MessageGroup) diff --git a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx index 34f6ede5..9dbea78c 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx @@ -35,7 +35,7 @@ const MessageGroupMenuBar: FC = ({ onDelete }) => { return ( - + {['fold', 'vertical', 'horizontal', 'grid'].map((layout) => ( @@ -93,6 +93,7 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>` flex-direction: row; align-items: center; gap: 10px; + margin: 0 20px; padding: 6px 10px; border-radius: 6px; margin-top: 10px; @@ -100,7 +101,6 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>` overflow: hidden; border: 0.5px solid var(--color-border); height: 40px; - transition: all 0.3s ease; background-color: var(--color-background); ` diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 0b3e6f05..1405b90a 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -11,15 +11,22 @@ import { SyncOutlined, TranslationOutlined } from '@ant-design/icons' +import { UploadOutlined } from '@ant-design/icons' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import { TranslateLanguageOptions } from '@renderer/config/translate' import { modelGenerating } from '@renderer/hooks/useRuntime' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { resetAssistantMessage } from '@renderer/services/MessagesService' +import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService' import { translateText } from '@renderer/services/TranslateService' import { Message, Model } from '@renderer/types' -import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils' +import { + captureScrollableDivAsBlob, + captureScrollableDivAsDataURL, + removeTrailingDoubleSpaces, + uuid +} from '@renderer/utils' +import { exportMarkdownToNotion, exportMessageAsMarkdown, messageToMarkdown } from '@renderer/utils/export' import { Button, Dropdown, Popconfirm, Tooltip } from 'antd' import dayjs from 'dayjs' import { isEmpty } from 'lodash' @@ -35,6 +42,7 @@ interface Props { isGrouped?: boolean isLastMessage: boolean isAssistantMessage: boolean + messageContainerRef: React.RefObject setModel: (model: Model) => void onEditMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => Promise @@ -50,6 +58,7 @@ const MessageMenubar: FC = (props) => { isLastMessage, isAssistantMessage, assistantModel, + messageContainerRef, onEditMessage, onDeleteMessage, onGetMessages @@ -194,9 +203,61 @@ const MessageMenubar: FC = (props) => { key: 'new-branch', icon: , onClick: onNewBranch + }, + { + label: t('chat.topics.export.title'), + key: 'export', + icon: , + children: [ + { + label: t('chat.topics.copy.image'), + key: 'img', + onClick: async () => { + await captureScrollableDivAsBlob(messageContainerRef, async (blob) => { + if (blob) { + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + } + }) + } + }, + { + label: t('chat.topics.export.image'), + key: 'image', + onClick: async () => { + const imageData = await captureScrollableDivAsDataURL(messageContainerRef) + const title = getMessageTitle(message) + if (title && imageData) { + window.api.file.saveImage(title, imageData) + } + } + }, + { + label: t('chat.topics.export.md'), + key: 'markdown', + onClick: () => exportMessageAsMarkdown(message) + }, + + { + label: t('chat.topics.export.word'), + key: 'word', + onClick: async () => { + const markdown = messageToMarkdown(message) + window.api.export.toWord(markdown, getMessageTitle(message)) + } + }, + { + label: t('chat.topics.export.notion'), + key: 'notion', + onClick: async () => { + const title = getMessageTitle(message) + const markdown = messageToMarkdown(message) + exportMarkdownToNotion(title, markdown) + } + } + ] } ], - [message, onEdit, onNewBranch, t] + [message, messageContainerRef, onEdit, onNewBranch, t] ) const onRegenerate = async (e: React.MouseEvent | undefined) => { diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index da8ad041..2716ef13 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -347,7 +347,6 @@ const LoaderContainer = styled.div` const ScrollContainer = styled.div` display: flex; flex-direction: column-reverse; - padding: 0 20px; ` interface ContainerProps { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index b812d99a..a306056a 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -1,7 +1,6 @@ import { ClearOutlined, CloseOutlined, - CopyOutlined, DeleteOutlined, EditOutlined, FolderOutlined, @@ -10,6 +9,7 @@ import { UploadOutlined } from '@ant-design/icons' import DragableList from '@renderer/components/DragableList' +import CopyIcon from '@renderer/components/Icons/CopyIcon' import PromptPopup from '@renderer/components/Popups/PromptPopup' import Scrollbar from '@renderer/components/Scrollbar' import { isMac } from '@renderer/config/constant' @@ -23,7 +23,7 @@ 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 { exportMarkdownToNotion, exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export' import { Dropdown, MenuProps, Tooltip } from 'antd' import dayjs from 'dayjs' import { findIndex } from 'lodash' @@ -194,7 +194,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic { label: t('chat.topics.copy.title'), key: 'copy', - icon: , + icon: , children: [ { label: t('chat.topics.copy.image'), @@ -235,7 +235,10 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic { label: t('chat.topics.export.notion'), key: 'notion', - onClick: () => exportTopicToNotion(topic) + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToNotion(topic.name, markdown) + } } ] } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 809c03bf..e7651d44 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -5,6 +5,7 @@ import i18n from '@renderer/i18n' import store from '@renderer/store' import { Assistant, Message, Model, Topic } from '@renderer/types' import { uuid } from '@renderer/utils' +import dayjs from 'dayjs' import { isEmpty, remove, takeRight } from 'lodash' import { NavigateFunction } from 'react-router' @@ -168,3 +169,27 @@ export function resetAssistantMessage(message: Message, model?: Model): Message useful: undefined } } + +export function getMessageTitle(message: Message, length = 30) { + let title = message.content.split('\n')[0] + + if (title.includes('.')) { + title = title.split('.')[0] + } else if (title.includes(',')) { + title = title.split(',')[0] + } else if (title.includes(',')) { + title = title.split(',')[0] + } else if (title.includes('。')) { + title = title.split('。')[0] + } + + if (title.length > length) { + title = title.slice(0, length) + } + + if (!title) { + title = dayjs(message.createdAt).format('YYYYMMDDHHmm') + } + + return title +} diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index 5ce759af..153d6a1e 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -1,6 +1,7 @@ import { Client } from '@notionhq/client' import db from '@renderer/databases' import i18n from '@renderer/i18n' +import { getMessageTitle } from '@renderer/services/MessagesService' import store from '@renderer/store' import { setExportState } from '@renderer/store/runtime' import { Message, Topic } from '@renderer/types' @@ -34,24 +35,32 @@ export const exportTopicAsMarkdown = async (topic: Topic) => { window.api.file.save(fileName, markdown) } -export const exportTopicToNotion = async (topic: Topic) => { +export const exportMessageAsMarkdown = async (message: Message) => { + const fileName = getMessageTitle(message) + '.md' + const markdown = messageToMarkdown(message) + window.api.file.save(fileName, markdown) +} + +export const exportMarkdownToNotion = async (title: string, content: string) => { const { isExporting } = store.getState().runtime.export + if (isExporting) { window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' }) return } - setExportState({ - isExporting: true - }) + + setExportState({ isExporting: true }) + const { notionDatabaseID, notionApiKey } = store.getState().settings + if (!notionApiKey || !notionDatabaseID) { window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' }) return } + try { const notion = new Client({ auth: notionApiKey }) - const markdown = await topicToMarkdown(topic) - const requestBody = JSON.stringify({ md: markdown }) + const requestBody = JSON.stringify({ md: content }) const res = await fetch('https://md2notion.hilars.dev', { method: 'POST', @@ -68,10 +77,10 @@ export const exportTopicToNotion = async (topic: Topic) => { parent: { database_id: notionDatabaseID }, properties: { [store.getState().settings.notionPageNameKey || 'Name']: { - title: [{ text: { content: topic.name } }] + title: [{ text: { content: title } }] } }, - children: notionBlocks // 使用转换后的块 + children: notionBlocks }) window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' }) diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index e6463a18..391473d1 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -344,6 +344,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject) => { return captureScrollableDiv(divRef).then((canvas) => { if (canvas) { @@ -352,11 +353,13 @@ export const captureScrollableDivAsDataURL = 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)