diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 9cc3a1d991..8a0f266579 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -21,6 +21,8 @@ export enum IpcChannel { App_InstallUvBinary = 'app:install-uv-binary', App_InstallBunBinary = 'app:install-bun-binary', + App_QuoteToMain = 'app:quote-to-main', + Notification_Send = 'notification:send', Notification_OnClick = 'notification:on-click', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 5339e81691..a8f02fe543 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -351,4 +351,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // selection assistant SelectionService.registerIpcHandler() + + ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text)) } diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 1cdcc2754d..f5bcfe88f9 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -544,6 +544,25 @@ export class WindowService { public setPinMiniWindow(isPinned) { this.isPinnedMiniWindow = isPinned } + + /** + * 引用文本到主窗口 + * @param text 原始文本(未格式化) + */ + public quoteToMainWindow(text: string): void { + try { + this.showMainWindow() + + const mainWindow = this.getMainWindow() + if (mainWindow && !mainWindow.isDestroyed()) { + setTimeout(() => { + mainWindow.webContents.send(IpcChannel.App_QuoteToMain, text) + }, 100) + } + } catch (error) { + Logger.error('Failed to quote to main window:', error as Error) + } + } } export const windowService = WindowService.getInstance() diff --git a/src/preload/index.ts b/src/preload/index.ts index 4ac4b39e9e..ae23883f9a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -228,7 +228,8 @@ const api = { closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose), minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize), pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned) - } + }, + quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text) } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx index 61d51f3701..bb3136067c 100644 --- a/src/renderer/src/components/ContextMenu/index.tsx +++ b/src/renderer/src/components/ContextMenu/index.tsx @@ -1,4 +1,3 @@ -import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Dropdown } from 'antd' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +11,6 @@ interface ContextMenuProps { const ContextMenu: React.FC = ({ children, onContextMenu }) => { const { t } = useTranslation() const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null) - const [selectedQuoteText, setSelectedQuoteText] = useState('') const [selectedText, setSelectedText] = useState('') const handleContextMenu = useCallback( @@ -20,12 +18,6 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) => e.preventDefault() const _selectedText = window.getSelection()?.toString() if (_selectedText) { - const quotedText = - _selectedText - .split('\n') - .map((line) => `> ${line}`) - .join('\n') + '\n-------------' - setSelectedQuoteText(quotedText) setContextMenuPosition({ x: e.clientX, y: e.clientY }) setSelectedText(_selectedText) } @@ -45,7 +37,7 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) => }, []) // 获取右键菜单项 - const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [ + const getContextMenuItems = (t: (key: string) => string, selectedText: string) => [ { key: 'copy', label: t('common.copy'), @@ -66,8 +58,8 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) => key: 'quote', label: t('chat.message.quote'), onClick: () => { - if (selectedQuoteText) { - EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) + if (selectedText) { + window.api?.quoteToMainWindow(selectedText) } } } @@ -78,7 +70,7 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) => {contextMenuPosition && (
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a9aa935dfd..12f96d748d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1876,7 +1876,8 @@ "summary": "Summarize", "search": "Search", "refine": "Refine", - "copy": "Copy" + "copy": "Copy", + "quote": "Quote" }, "window": { "pin": "Pin", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 0c558b7d47..9b96b9f706 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1876,7 +1876,8 @@ "summary": "要約", "search": "検索", "refine": "最適化", - "copy": "コピー" + "copy": "コピー", + "quote": "引用" }, "window": { "pin": "最前面に固定", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b461be3c76..21e3adad95 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1876,7 +1876,8 @@ "summary": "Суммаризировать", "search": "Поиск", "refine": "Уточнить", - "copy": "Копировать" + "copy": "Копировать", + "quote": "Цитировать" }, "window": { "pin": "Закрепить", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 67f233915f..fb81a89108 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1876,7 +1876,8 @@ "summary": "总结", "search": "搜索", "refine": "优化", - "copy": "复制" + "copy": "复制", + "quote": "引用" }, "window": { "pin": "置顶", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index bb337fa365..b7c1ad01dd 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1876,7 +1876,8 @@ "summary": "總結", "search": "搜尋", "refine": "優化", - "copy": "複製" + "copy": "複製", + "quote": "引用" }, "window": { "pin": "置頂", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 0f8c23d276..e127cc375d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -33,8 +33,10 @@ import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' +import { formatQuotedText } from '@renderer/utils/formats' import { getFilesFromDropEvent } from '@renderer/utils/input' import { documentExts, imageExts, textExts } from '@shared/config/constant' +import { IpcChannel } from '@shared/IpcChannel' import { Button, Tooltip } from 'antd' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import dayjs from 'dayjs' @@ -419,6 +421,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) }, [addTopic, assistant, setActiveTopic, setModel]) + const onQuote = useCallback( + (text: string) => { + const quotedText = formatQuotedText(text) + setText((prevText) => { + const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n` + setTimeout(() => resizeTextArea(), 0) + return newText + }) + textareaRef.current?.focus() + }, + [resizeTextArea] + ) + const onPause = async () => { await pauseMessages() } @@ -623,18 +638,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = _setEstimateTokenCount(tokensCount) setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 }), - EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic), - EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => { - setText((prevText) => { - const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n` - setTimeout(() => resizeTextArea(), 0) - return newText - }) - textareaRef.current?.focus() - }) + EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic) ] - return () => unsubscribes.forEach((unsub) => unsub()) - }, [addNewTopic, resizeTextArea]) + + // 监听引用事件 + const quoteFromAnywhereRemover = window.electron?.ipcRenderer.on( + IpcChannel.App_QuoteToMain, + (_, selectedText: string) => onQuote(selectedText) + ) + + return () => { + unsubscribes.forEach((unsub) => unsub()) + quoteFromAnywhereRemover?.() + } + }, [addNewTopic, onQuote]) useEffect(() => { textareaRef.current?.focus() diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index 84ed4ad8fa..354a11ea7f 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -193,7 +193,7 @@ const SelectionAssistantSettings: FC = () => { - 高级 + {t('selection.settings.advanced.title')} diff --git a/src/renderer/src/services/EventService.ts b/src/renderer/src/services/EventService.ts index 27e552506c..498beed049 100644 --- a/src/renderer/src/services/EventService.ts +++ b/src/renderer/src/services/EventService.ts @@ -27,7 +27,6 @@ export const EVENT_NAMES = { ADD_NEW_TOPIC: 'ADD_NEW_TOPIC', RESEND_MESSAGE: 'RESEND_MESSAGE', SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR', - QUOTE_TEXT: 'QUOTE_TEXT', EDIT_CODE_BLOCK: 'EDIT_CODE_BLOCK', CHANGE_TOPIC: 'CHANGE_TOPIC' } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 5ce6c46014..d0ded856a0 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -50,7 +50,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 110, + version: 111, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index abec96fa7f..53b50f7729 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -14,6 +14,7 @@ import { RootState } from '.' import { DEFAULT_TOOL_ORDER } from './inputTools' import { INITIAL_PROVIDERS, moveProvider } from './llm' import { mcpSlice } from './mcp' +import { defaultActionItems } from './selectionStore' import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings' import { defaultWebSearchProviders } from './websearch' @@ -77,6 +78,17 @@ function updateWebSearchProvider(state: RootState, provider: Partial item.id === id)) { + const action = defaultActionItems.find((item) => item.id === id) + if (action) { + state.selectionStore.actionItems.push(action) + } + } + } +} + const migrateConfig = { '2': (state: RootState) => { try { @@ -1485,6 +1497,14 @@ const migrateConfig = { } catch (error) { return state } + }, + '111': (state: RootState) => { + try { + addSelectionAction(state, 'quote') + return state + } catch (error) { + return state + } } } diff --git a/src/renderer/src/store/selectionStore.ts b/src/renderer/src/store/selectionStore.ts index eecf9b7fe2..0eaecb25fa 100644 --- a/src/renderer/src/store/selectionStore.ts +++ b/src/renderer/src/store/selectionStore.ts @@ -14,7 +14,8 @@ export const defaultActionItems: ActionItem[] = [ searchEngine: 'Google|https://www.google.com/search?q={{queryString}}' }, { id: 'copy', name: 'selection.action.builtin.copy', enabled: true, isBuiltIn: true, icon: 'clipboard-copy' }, - { id: 'refine', name: 'selection.action.builtin.refine', enabled: false, isBuiltIn: true, icon: 'wand-sparkles' } + { id: 'refine', name: 'selection.action.builtin.refine', enabled: false, isBuiltIn: true, icon: 'wand-sparkles' }, + { id: 'quote', name: 'selection.action.builtin.quote', enabled: false, isBuiltIn: true, icon: 'quote' } ] export const initialState: SelectionState = { diff --git a/src/renderer/src/utils/formats.ts b/src/renderer/src/utils/formats.ts index a99a7c8a14..559b4e7a52 100644 --- a/src/renderer/src/utils/formats.ts +++ b/src/renderer/src/utils/formats.ts @@ -179,3 +179,12 @@ export function addImageFileToContents(messages: Message[]) { return messages.map((message) => (message.id === lastAssistantMessage.id ? updatedAssistantMessage : message)) } + +export function formatQuotedText(text: string) { + return ( + text + .split('\n') + .map((line) => `> ${line}`) + .join('\n') + '\n-------------' + ) +} diff --git a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx index 07103f4c6b..21c54a923f 100644 --- a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx +++ b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx @@ -188,6 +188,9 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { case 'search': handleSearch(newAction) break + case 'quote': + handleQuote(newAction) + break default: handleDefaultAction(newAction) break @@ -220,6 +223,16 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { window.api?.selection.hideToolbar() } + /** + * Quote the selected text to the inputbar of the main window + */ + const handleQuote = (action: ActionItem) => { + if (action.selectedText) { + window.api?.quoteToMainWindow(action.selectedText) + window.api?.selection.hideToolbar() + } + } + const handleDefaultAction = (action: ActionItem) => { window.api?.selection.processAction(action) window.api?.selection.hideToolbar()