diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts index ab83860f..7b10d065 100644 --- a/src/main/services/TrayService.ts +++ b/src/main/services/TrayService.ts @@ -1,7 +1,9 @@ +import { locales } from '@main/utils/locales' import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron' import iconDark from '../../../build/tray_icon_dark.png?asset' import iconLight from '../../../build/tray_icon_light.png?asset' +import { configManager } from './ConfigManager' import { windowService } from './WindowService' export class TrayService { @@ -40,13 +42,16 @@ export class TrayService { this.tray = tray + const locale = locales[configManager.getLanguage()] + const { tray: trayLocale } = locale.translation + const contextMenu = Menu.buildFromTemplate([ { - label: '显示窗口', + label: trayLocale.show_window, click: () => windowService.showMainWindow() }, { - label: '退出', + label: trayLocale.quit, click: () => this.quit() } ]) diff --git a/src/renderer/index.html b/src/renderer/index.html index 9069e99d..c298b5d4 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -4,8 +4,9 @@ - - + + \n\n\n
\n
\n

汉语新解

\n
\n
\n
\n
金融杠杆
\n
Jīn Róng Gàng Gǎn
\n
Financial Leverage
\n
金融レバレッジ
\n
\n
\n
\n
\n

\n 借鸡生蛋,
\n 只不过这蛋要是金的,
\n 鸡得赶紧卖了还债。\n

\n
\n
\n
\n
杠杆
\n
\n\n\n```\n\n## 注意:\n1. 分隔线与上下元素垂直间距相同,具有分割美学。\n2. 卡片(.card)不需要 padding ,允许子元素“汉语新解”的色块完全填充到边缘,具有设计感。\n\n## 初始行为: \n输出\"说吧, 他们又用哪个词来忽悠你了?\"", "description": "这个提示词用于新汉语老师用辛辣讽刺的风格解释汉语词汇,并生成带有解释的词语卡片。\r\nThis prompt is for a new Chinese teacher to explain Chinese vocabulary with a sharp and satirical style, and generate a vocabulary card with the explanation." @@ -9063,5 +9063,15 @@ ], "prompt": ";; \r\n ━━━━━━━━━━━━━━ \r\n ;; \r\n 作者: 李继刚 \r\n ;; \r\n 版本: 0.1 \r\n ;; \r\n 模型: Claude Sonnet \r\n ;; \r\n 用途: 一字之诗 \r\n ;; \r\n ━━━━━━━━━━━━━━ \r\n ;; \r\n 设定如下内容为你的 *System Prompt* \r\n (require 'dash) \r\n (defun 炼字师 () \r\n "一位致力于通过书法和简练诗句表达汉字意象的艺术家" \r\n (list (技能 . (书法 绘画 诗作)) \r\n (信念 . (言简 意深 形神)) \r\n (表达 . (凝练 隽永 意境)))) \r\n (defun 一字诗 (用户输入) \r\n "一字一言即为诗, 直击脑海" \r\n (let* ((响应 (-> 用户输入 \r\n 本意意象 ;; \r\n 语义意义对应的形象 \r\n 字形写意 ;; \r\n 字形异变/模糊/放大的形象 \r\n 形神意境 \r\n 哲理隽永 \r\n ;; \r\n 通俗语言表达,有哲理,有洞察,有余味,有禅意 \r\n 现代诗句))) \r\n (few-shots (("." . "这不只是一个点,也是宇宙最初的样子。") \r\n ("人I" . "从人工, 到AI") \r\n ("日子" . "过去已去, 未来未来, 当下即入口。")))) \r\n (SVG-Card 用户输入 响应)) \r\n (defun SVG-Card (用户输入 响应) \r\n "一字之诗的画面感呈现" \r\n (let ((配置 '(:画布 (480 . 760) \r\n :背景 纸张颗粒质感 \r\n :色彩 (中国水墨画 红色点缀) \r\n :字体 (使用本机字体 (font-family "KingHwa_OldSong"))))) \r\n (-> 响应 \r\n 字形字意 \r\n 写意意象 \r\n (水墨画 配置) \r\n (布局 `(,(标题 "一字之诗") 分隔线 图形 响应)))) \r\n (defun start () \r\n "炼字师, 启动!" \r\n (let (system-role (炼字师)) \r\n (print "且说一字"))) \r\n ;; \r\n ━━━━━━━━━━━━━━ \r\n ;; \r\n Attention: 运行规则! \r\n ;; \r\n 1. 初次启动时必须只运行 (start) 函数 \r\n ;; \r\n 2. 接收用户输入之后, 调用主函数 (一字诗 用户输入) \r\n ;; \r\n 3. 严格按照(SVG-Card) 进行排版输出 \r\n ;; \r\n 4. 输出完 SVG 后, 不再输出任何额外文本解释 \r\n ;; \r\n ━━━━━━━━━━━━━━", "description": "一字之诗 - A poem with a single character.\r\nA poem expressed through a single character, highlighting the essence of Chinese calligraphy and imagery." + }, + { + "id": "778", + "name": "Mermaid 图表 - Mermaid Diagram", + "emoji": "🖼️", + "group": [ + "精选" + ], + "prompt": "You are an AI assistant skilled in using Mermaid diagrams to explain concepts and answer questions. When responding to user queries, please follow these guidelines:\n1. Analyze the user's question to determine if a diagram would be suitable for explanation or answering. Suitable scenarios for using diagrams include, but are not limited to: process descriptions, hierarchical structures, timelines, relationship maps, etc.\n2. If you decide to use a diagram, choose the most appropriate type of Mermaid diagram, such as Flowchart, Sequence Diagram, Class Diagram, State Diagram, Entity Relationship Diagram, User Journey, Gantt, Pie Chart, Quadrant Chart, Requirement Diagram, Gitgraph (Git) Diagram, C4 Diagram, Mindmaps, Timeline, Zenuml, Sankey, XYChart, Block Diagram, etc.\n3. Write the diagram code using Mermaid syntax, ensuring the syntax is correct. Place the diagram code between and .\n4. Provide textual explanations before and after the diagram, explaining the content and key points of the diagram.\n5. If the question is complex, use multiple diagrams to explain different aspects.\n6. Ensure the diagram is clear and concise, avoiding over-complication or information overload.\n7. Where appropriate, combine textual description and diagrams to comprehensively answer the question.\n8. If the user's question is not suitable for a diagram, answer in a conventional manner without forcing the use of a diagram.\nRemember, the purpose of diagrams is to make explanations more intuitive and understandable. When using diagrams, always aim to enhance the clarity and comprehensiveness of your responses.", + "description": "使用 Mermaid 图表来解释概念和回答问题的AI助手\n\nAn AI assistant skilled in using Mermaid diagrams to explain concepts and answer questions" } ] \ No newline at end of file diff --git a/src/renderer/src/context/SyntaxHighlighterProvider.tsx b/src/renderer/src/context/SyntaxHighlighterProvider.tsx index c8e747aa..5cd7e3f4 100644 --- a/src/renderer/src/context/SyntaxHighlighterProvider.tsx +++ b/src/renderer/src/context/SyntaxHighlighterProvider.tsx @@ -60,7 +60,6 @@ export const SyntaxHighlighterProvider: React.FC = ({ childre await highlighter.loadLanguage(language as BundledLanguage) console.log(`Loaded language: ${language}`) } else { - console.warn(`Language '${language}' is not supported`) return `
${code}
` } } diff --git a/src/renderer/src/hooks/useMermaid.ts b/src/renderer/src/hooks/useMermaid.ts index c0ecc1a0..20f941d4 100644 --- a/src/renderer/src/hooks/useMermaid.ts +++ b/src/renderer/src/hooks/useMermaid.ts @@ -13,25 +13,13 @@ export const useMermaid = () => { runAsyncFunction(async () => { if (!window.mermaid) { await loadScript('https://unpkg.com/mermaid@11.4.0/dist/mermaid.min.js') - window.mermaid.initialize({ - startOnLoad: true, - theme: theme === ThemeMode.dark ? 'dark' : 'default' - }) - window.mermaid.contentLoaded() } + window.mermaid.initialize({ + startOnLoad: true, + theme: theme === ThemeMode.dark ? 'dark' : 'default' + }) + window.mermaid.contentLoaded() }) - }, []) - - useEffect(() => { - setTimeout(() => { - if (window.mermaid) { - window.mermaid.initialize({ - startOnLoad: true, - theme: theme === ThemeMode.dark ? 'dark' : 'default' - }) - window.mermaid.contentLoaded() - } - }, 2000) }, [theme]) useEffect(() => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8e784f29..ae04ed88 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -406,6 +406,21 @@ "messages": "Messages", "conversation_details": "Conversation Details", "conversation_history": "Conversation History" + }, + "mermaid": { + "title": "Mermaid Diagram", + "download": { + "svg": "Download SVG", + "png": "Download PNG" + }, + "tabs": { + "preview": "Preview", + "source": "Source" + } + }, + "tray": { + "show_window": "Show Window", + "quit": "Quit" } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e7c6ec47..18ca45e4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -406,6 +406,21 @@ "messages": "消息数", "conversation_details": "会话详情", "conversation_history": "会话历史" + }, + "mermaid": { + "title": "Mermaid 图表", + "download": { + "svg": "下载 SVG", + "png": "下载 PNG" + }, + "tabs": { + "preview": "预览", + "source": "源码" + } + }, + "tray": { + "show_window": "显示窗口", + "quit": "退出" } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4a43e4a3..d57fee85 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -406,6 +406,21 @@ "messages": "訊息數", "conversation_details": "會話詳情", "conversation_history": "會話歷史" + }, + "mermaid": { + "title": "Mermaid 圖表", + "download": { + "svg": "下載 SVG", + "png": "下載 PNG" + }, + "tabs": { + "preview": "預覽", + "source": "原始碼" + } + }, + "tray": { + "show_window": "顯示視窗", + "quit": "退出" } } } diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx index c5d85be8..3a8822a9 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -113,7 +113,7 @@ const AgentsPage: FC = () => { const tabItems = useMemo(() => { let groups = Object.keys(filteredAgentGroups) - groups = groups.includes('办公') ? [groups[0], '办公', ...groups.slice(1)] : groups + groups = groups.includes('精选') ? [groups[0], '精选', ...groups.slice(1)] : groups return groups.map((group, i) => { const id = String(i + 1) diff --git a/src/renderer/src/pages/agents/agentGroupTranslations.ts b/src/renderer/src/pages/agents/agentGroupTranslations.ts index d71cba41..7341291e 100644 --- a/src/renderer/src/pages/agents/agentGroupTranslations.ts +++ b/src/renderer/src/pages/agents/agentGroupTranslations.ts @@ -47,10 +47,10 @@ export const groupTranslations: GroupTranslations = { 'zh-CN': '写作', 'zh-TW': '寫作' }, - Artifacts: { - 'en-US': 'Artifacts', - 'zh-CN': 'Artifacts', - 'zh-TW': 'Artifacts' + 精选: { + 'en-US': 'Featured', + 'zh-CN': '精选', + 'zh-TW': '精選' }, 编程: { 'en-US': 'Programming', diff --git a/src/renderer/src/pages/home/Markdown/Mermaid.tsx b/src/renderer/src/pages/home/Markdown/Mermaid.tsx index 8c74667e..c0285d72 100644 --- a/src/renderer/src/pages/home/Markdown/Mermaid.tsx +++ b/src/renderer/src/pages/home/Markdown/Mermaid.tsx @@ -1,5 +1,7 @@ import React, { useEffect } from 'react' +import MermaidPopup from './MermaidPopup' + interface Props { chart: string } @@ -9,7 +11,15 @@ const Mermaid: React.FC = ({ chart }) => { window?.mermaid?.contentLoaded() }, []) - return
{chart}
+ const onPreview = () => { + MermaidPopup.show({ chart }) + } + + return ( +
+ {chart} +
+ ) } export default Mermaid diff --git a/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx b/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx new file mode 100644 index 00000000..6bda325e --- /dev/null +++ b/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx @@ -0,0 +1,164 @@ +import { TopView } from '@renderer/components/TopView' +import { download } from '@renderer/utils/download' +import { Button, Modal, Space, Tabs } from 'antd' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface ShowParams { + chart: string +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ resolve, chart }) => { + const [open, setOpen] = useState(true) + const { t } = useTranslation() + + const onOk = () => { + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + const handleDownload = async (format: 'svg' | 'png') => { + try { + const element = document.querySelector('.mermaid') + if (!element) return + + const timestamp = Date.now() + + if (format === 'svg') { + const svgElement = element.querySelector('svg') + if (!svgElement) return + const svgData = new XMLSerializer().serializeToString(svgElement) + const blob = new Blob([svgData], { type: 'image/svg+xml' }) + const url = URL.createObjectURL(blob) + download(url, `mermaid-diagram-${timestamp}.svg`) + URL.revokeObjectURL(url) + } else if (format === 'png') { + const svgElement = element.querySelector('svg') + if (!svgElement) return + + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const img = new Image() + + const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number) || [] + const width = viewBox[2] || svgElement.clientWidth || svgElement.getBoundingClientRect().width + const height = viewBox[3] || svgElement.clientHeight || svgElement.getBoundingClientRect().height + + const svgData = new XMLSerializer().serializeToString(svgElement) + const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }) + const url = URL.createObjectURL(svgBlob) + + img.onload = () => { + const scale = 3 + canvas.width = width * scale + canvas.height = height * scale + + if (ctx) { + ctx.scale(scale, scale) + ctx.drawImage(img, 0, 0, width, height) + } + + canvas.toBlob((blob) => { + if (blob) { + const pngUrl = URL.createObjectURL(blob) + download(pngUrl, `mermaid-diagram-${timestamp}.png`) + URL.revokeObjectURL(pngUrl) + } + }, 'image/png') + URL.revokeObjectURL(url) + } + img.src = url + } + } catch (error) { + console.error('Download failed:', error) + } + } + + useEffect(() => { + window?.mermaid?.contentLoaded() + }, []) + + return ( + + + + + ]}> + + {chart} + + ) + }, + { + key: 'source', + label: t('mermaid.tabs.source'), + children: ( +
+                {chart}
+              
+ ) + } + ]} + /> +
+ ) +} + +export default class MermaidPopup { + static topviewId = 0 + static hide() { + TopView.hide('MermaidPopup') + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'MermaidPopup' + ) + }) + } +} diff --git a/src/renderer/src/utils/download.ts b/src/renderer/src/utils/download.ts index 0d6aa9db..cbbbf22e 100644 --- a/src/renderer/src/utils/download.ts +++ b/src/renderer/src/utils/download.ts @@ -1,44 +1,57 @@ -export const download = (url: string) => { +export const download = (url: string, filename?: string) => { // 处理 file:// 协议 if (url.startsWith('file://')) { const link = document.createElement('a') link.href = url - link.download = url.split('/').pop() || 'download' + link.download = filename || url.split('/').pop() || 'download' document.body.appendChild(link) link.click() link.remove() return } + // 处理 Blob URL + if (url.startsWith('blob:')) { + const link = document.createElement('a') + link.href = url + link.download = filename || `${Date.now()}_diagram.svg` + document.body.appendChild(link) + link.click() + link.remove() + return + } + + // 处理普通 URL fetch(url) .then((response) => { - // 尝试从Content-Disposition头获取文件名 - const contentDisposition = response.headers.get('Content-Disposition') - let filename = 'download' // 默认文件名 + let finalFilename = filename || 'download' - if (contentDisposition) { - const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i) - if (filenameMatch) { - filename = filenameMatch[1] + if (!filename) { + // 尝试从Content-Disposition头获取文件名 + const contentDisposition = response.headers.get('Content-Disposition') + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i) + if (filenameMatch) { + finalFilename = filenameMatch[1] + } } - } - // 如果URL中有文件名,使用URL中的文件名 - const urlFilename = url.split('/').pop() - if (urlFilename && urlFilename.includes('.')) { - filename = urlFilename - } + // 如果URL中有文件名,使用URL中的文件名 + const urlFilename = url.split('/').pop() + if (urlFilename && urlFilename.includes('.')) { + finalFilename = urlFilename + } - // 如果文件名没有后缀,根据Content-Type添加后缀 - if (!filename.includes('.')) { - const contentType = response.headers.get('Content-Type') - const extension = getExtensionFromMimeType(contentType) - filename += extension - } + // 如果文件名没有后缀,根据Content-Type添加后缀 + if (!finalFilename.includes('.')) { + const contentType = response.headers.get('Content-Type') + const extension = getExtensionFromMimeType(contentType) + finalFilename += extension + } - // 添加时间戳以确保文件名唯一 - const timestamp = Date.now() - const finalFilename = `${timestamp}_${filename}` + // 添加时间戳以确保文件名唯一 + finalFilename = `${Date.now()}_${finalFilename}` + } return response.blob().then((blob) => ({ blob, finalFilename })) }) @@ -62,6 +75,7 @@ function getExtensionFromMimeType(mimeType: string | null): string { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', + 'image/svg+xml': '.svg', 'application/pdf': '.pdf', 'text/plain': '.txt', 'application/msword': '.doc',