From 7be8ece638087013ac85834b364b77777ceb68ac Mon Sep 17 00:00:00 2001 From: Ctrlz <143257420+ctrlz526@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:13:56 +0800 Subject: [PATCH] feat: add new Markdown components and enhance i18n support (#5622) * feat: add new Markdown components and enhance i18n support (cherry picked from commit b0b6cc7ad49ac35f070389744a764928d7103074) * feat: support structured data render --------- Co-authored-by: FinleyGe --- packages/web/i18n/en/common.json | 3 + packages/web/i18n/zh-CN/common.json | 3 + packages/web/i18n/zh-Hant/common.json | 3 + pnpm-lock.yaml | 105 +++++- projects/app/package.json | 3 + projects/app/src/components/Markdown/A.tsx | 1 - .../components/Markdown/codeBlock/Divider.tsx | 8 + .../Markdown/codeBlock/IndicatorCard.tsx | 65 ++++ .../components/Markdown/codeBlock/Link.tsx | 31 ++ .../components/Markdown/codeBlock/Table.tsx | 142 +++++++ .../Markdown/codeBlock/TextBlock.tsx | 88 +++++ .../components/Markdown/codeBlock/Tips.tsx | 36 ++ .../Markdown/img/EChartsCodeBlock.tsx | 355 ++++++++++++++++-- .../app/src/components/Markdown/index.tsx | 149 +++++++- projects/app/src/components/Markdown/utils.ts | 9 +- 15 files changed, 948 insertions(+), 53 deletions(-) create mode 100644 projects/app/src/components/Markdown/codeBlock/Divider.tsx create mode 100644 projects/app/src/components/Markdown/codeBlock/IndicatorCard.tsx create mode 100644 projects/app/src/components/Markdown/codeBlock/Link.tsx create mode 100644 projects/app/src/components/Markdown/codeBlock/Table.tsx create mode 100644 projects/app/src/components/Markdown/codeBlock/TextBlock.tsx create mode 100644 projects/app/src/components/Markdown/codeBlock/Tips.tsx diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index df4a2bd8d..2ca63aaee 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -372,6 +372,8 @@ "core.chat.feedback.Feedback Close": "Close Feedback", "core.chat.feedback.No Content": "User Did Not Provide Specific Feedback Content", "core.chat.feedback.Read User dislike": "User Disagrees\nClick to View Content", + "core.chat.indicator.no_data": "There is no data yet", + "core.chat.table.per_page": "{{num}} per page", "core.chat.logs.api": "API Call", "core.chat.logs.feishu": "Feishu", "core.chat.logs.free_login": "No login link", @@ -392,6 +394,7 @@ "core.chat.quote.beforeUpdate": "Before update", "core.chat.response.Complete Response": "Complete Response", "core.chat.response.Extension model": "Question Optimization Model", + "core.chat.response.Fold response": "Fold", "core.chat.response.Read complete response": "View Details", "core.chat.response.Read complete response tips": "Click to View Detailed Process", "core.chat.response.Tool call input tokens": "Tool Call Input Tokens Consumption", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index f0f867f23..5d51454b3 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -372,6 +372,8 @@ "core.chat.feedback.Feedback Close": "关闭反馈", "core.chat.feedback.No Content": "用户没有填写具体反馈内容", "core.chat.feedback.Read User dislike": "用户表示反对\n点击查看内容", + "core.chat.indicator.no_data": "暂时没有该数据", + "core.chat.table.per_page": "{{num}} 条/页", "core.chat.logs.api": "API 调用", "core.chat.logs.feishu": "飞书", "core.chat.logs.free_login": "免登录链接", @@ -392,6 +394,7 @@ "core.chat.quote.beforeUpdate": "更新前", "core.chat.response.Complete Response": "完整响应", "core.chat.response.Extension model": "问题优化模型", + "core.chat.response.Fold response": "收起", "core.chat.response.Read complete response": "查看详情", "core.chat.response.Read complete response tips": "点击查看详细流程", "core.chat.response.Tool call input tokens": "工具调用输入 Tokens", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index ef9c3fba4..0668c14c8 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -372,6 +372,8 @@ "core.chat.feedback.Feedback Close": "關閉回饋", "core.chat.feedback.No Content": "使用者未提供具體回饋內容", "core.chat.feedback.Read User dislike": "使用者表示反對\n點選檢視內容", + "core.chat.indicator.no_data": "暫時沒有該數據", + "core.chat.table.per_page": "{{num}} 筆/頁", "core.chat.logs.api": "API 呼叫", "core.chat.logs.feishu": "飛書", "core.chat.logs.free_login": "免登入連結", @@ -392,6 +394,7 @@ "core.chat.quote.beforeUpdate": "更新前", "core.chat.response.Complete Response": "完整回應", "core.chat.response.Extension model": "問題最佳化模型", + "core.chat.response.Fold response": "收起", "core.chat.response.Read complete response": "檢視詳細資料", "core.chat.response.Read complete response tips": "點選檢視詳細流程", "core.chat.response.Tool call input tokens": "工具呼叫輸入 Token 消耗", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3a31fa1..9582b7152 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 10.1.4(socks@2.8.4) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) prettier: specifier: 3.2.4 version: 3.2.4 @@ -318,7 +318,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 2.10.7 version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -396,7 +396,7 @@ importers: version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -460,7 +460,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 2.10.7 version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -512,6 +512,9 @@ importers: dayjs: specifier: ^1.11.7 version: 1.11.13 + dompurify: + specifier: ^3.2.7 + version: 3.2.7 echarts: specifier: 5.4.1 version: 5.4.1 @@ -556,7 +559,7 @@ importers: version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -599,6 +602,9 @@ importers: rehype-katex: specifier: ^7.0.0 version: 7.0.1 + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -624,6 +630,9 @@ importers: '@svgr/webpack': specifier: ^6.5.1 version: 6.5.1 + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -3421,6 +3430,10 @@ packages: '@types/decompress@4.2.7': resolution: {integrity: sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3601,6 +3614,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/tunnel@0.0.4': resolution: {integrity: sha512-bQgDBL5XiqrrPUaZd9bZ2esOXcU4GTmgg0n6LHDqoMJezO3VFRZsW8qN6Gp64/LAmjtzNU3iAHBfV3Z2ht5DSg==} @@ -5097,6 +5113,9 @@ packages: dompurify@3.1.6: resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -5921,9 +5940,15 @@ packages: hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + hast-util-to-text@4.0.2: resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} @@ -5958,6 +5983,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -8019,6 +8047,9 @@ packages: property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} @@ -8343,6 +8374,9 @@ packages: rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + remark-breaks@4.0.0: resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} @@ -8900,7 +8934,7 @@ packages: superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supertest@6.3.4: resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} @@ -10795,7 +10829,7 @@ snapshots: '@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': + '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': dependencies: '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/cache': 11.14.0 @@ -12785,6 +12819,10 @@ snapshots: dependencies: '@types/node': 20.17.24 + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.2.7 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -13004,6 +13042,9 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/tunnel@0.0.4': dependencies: '@types/node': 20.17.24 @@ -14798,6 +14839,10 @@ snapshots: dompurify@3.1.6: {} + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -15140,7 +15185,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -15151,7 +15196,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -15173,7 +15218,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15202,7 +15247,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -16105,6 +16150,22 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.2.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.6 @@ -16125,6 +16186,16 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-text@4.0.2: dependencies: '@types/hast': 3.0.4 @@ -16170,6 +16241,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -18193,7 +18266,7 @@ snapshots: transitivePeerDependencies: - supports-color - next-i18next@15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-i18next@15.4.2(i18next@23.16.8)(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.10 '@types/hoist-non-react-statics': 3.3.6 @@ -18826,6 +18899,8 @@ snapshots: dependencies: xtend: 4.0.2 + property-information@6.5.0: {} + property-information@7.0.0: {} proto-list@1.2.4: {} @@ -19231,6 +19306,12 @@ snapshots: unist-util-visit-parents: 6.0.1 vfile: 6.0.3 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + remark-breaks@4.0.0: dependencies: '@types/mdast': 4.0.4 diff --git a/projects/app/package.json b/projects/app/package.json index 26fe8b21e..3b0e1390a 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -29,6 +29,7 @@ "axios": "^1.12.1", "date-fns": "2.30.0", "dayjs": "^1.11.7", + "dompurify": "^3.2.7", "echarts": "5.4.1", "echarts-gl": "2.0.9", "framer-motion": "9.1.7", @@ -58,6 +59,7 @@ "recharts": "^2.15.0", "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.0", + "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", @@ -68,6 +70,7 @@ }, "devDependencies": { "@svgr/webpack": "^6.5.1", + "@types/dompurify": "^3.2.0", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.3", "@types/lodash": "^4.14.191", diff --git a/projects/app/src/components/Markdown/A.tsx b/projects/app/src/components/Markdown/A.tsx index a3b93f763..75948f017 100644 --- a/projects/app/src/components/Markdown/A.tsx +++ b/projects/app/src/components/Markdown/A.tsx @@ -23,7 +23,6 @@ import Markdown from '.'; import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils'; import { Types } from 'mongoose'; import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; -import { useCreation } from 'ahooks'; export type AProps = { chatAuthData?: { diff --git a/projects/app/src/components/Markdown/codeBlock/Divider.tsx b/projects/app/src/components/Markdown/codeBlock/Divider.tsx new file mode 100644 index 000000000..4984804f7 --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/Divider.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { Box } from '@chakra-ui/react'; + +const Divider: React.FC = () => { + return ; +}; + +export default Divider; diff --git a/projects/app/src/components/Markdown/codeBlock/IndicatorCard.tsx b/projects/app/src/components/Markdown/codeBlock/IndicatorCard.tsx new file mode 100644 index 000000000..3e755a2a2 --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/IndicatorCard.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Box, Text, VStack, Flex } from '@chakra-ui/react'; +import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; + +type IndicatorCardProps = { + dataList: { + name: string; + value: string | number; + }[]; +}; + +const IndicatorCard: React.FC = ({ dataList }) => { + const { t } = useSafeTranslation(); + if (!dataList || !Array.isArray(dataList) || dataList.length === 0) { + return No indicator data available; + } + + return ( + + {dataList.map((indicator, index) => ( + + + + {/* indicator name */} + + + {indicator.name} + + + + {/* indicator value and unit */} + + + {indicator.value || t('common:core.chat.indicator.no_data')} + + + + + ))} + + ); +}; + +export default IndicatorCard; diff --git a/projects/app/src/components/Markdown/codeBlock/Link.tsx b/projects/app/src/components/Markdown/codeBlock/Link.tsx new file mode 100644 index 000000000..92e2a240a --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/Link.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box, Text, Link, Flex } from '@chakra-ui/react'; + +const LinkBlock: React.FC<{ data: { text: string; url: string } }> = ({ data }) => { + const handleClick = () => { + window.open(data.url, '_blank', 'noopener,noreferrer'); + }; + + return ( + + + + {data.text} + + + + ); +}; + +export default LinkBlock; diff --git a/projects/app/src/components/Markdown/codeBlock/Table.tsx b/projects/app/src/components/Markdown/codeBlock/Table.tsx new file mode 100644 index 000000000..c27e92bf3 --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/Table.tsx @@ -0,0 +1,142 @@ +import React, { useState, useMemo } from 'react'; +import { Box, Flex, Text, Grid } from '@chakra-ui/react'; +import Icon from '@fastgpt/web/components/common/Icon'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; + +const TableBlock: React.FC<{ code: string }> = ({ code }) => { + const { t } = useSafeTranslation(); + const tableData = JSON.parse(code); + const [currentPage, setCurrentPage] = useState(1); + const [perPage, setPerPage] = useState(10); + + const headers = Object.keys(tableData[0]); + + // calculate paginated data + const { paginatedData, totalPages } = useMemo(() => { + const total = Math.ceil(tableData.length / perPage); + const startIndex = (currentPage - 1) * perPage; + const endIndex = startIndex + perPage; + const paginated = tableData.slice(startIndex, endIndex); + + return { + paginatedData: paginated, + totalPages: total + }; + }, [tableData, currentPage, perPage]); + + const handlePrevPage = () => { + setCurrentPage((prev) => Math.max(prev - 1, 1)); + }; + + const handleNextPage = () => { + setCurrentPage((prev) => Math.min(prev + 1, totalPages)); + }; + + const handlePerPageChange = (value: string) => { + setPerPage(Number(value)); + setCurrentPage(1); // reset to first page + }; + + return ( + + + + + + {headers.map((header, index) => ( + + ))} + + + + {paginatedData.map((row: any, rowIndex: number) => ( + + {headers.map((header, colIndex) => ( + + ))} + + ))} + +
+ {header} +
+ {row[header] || ''} +
+
+ + + + + + {currentPage} / {totalPages} + + + + + {totalPages > 1 && ( + + + + )} + +
+ ); +}; + +export default TableBlock; diff --git a/projects/app/src/components/Markdown/codeBlock/TextBlock.tsx b/projects/app/src/components/Markdown/codeBlock/TextBlock.tsx new file mode 100644 index 000000000..53f5fd448 --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/TextBlock.tsx @@ -0,0 +1,88 @@ +import React, { useState, useMemo } from 'react'; +import { Box, Button, Collapse, Flex } from '@chakra-ui/react'; +import ReactMarkdown from 'react-markdown'; +import RemarkMath from 'remark-math'; +import RemarkBreaks from 'remark-breaks'; +import RehypeKatex from 'rehype-katex'; +import RemarkGfm from 'remark-gfm'; +import RehypeExternalLinks from 'rehype-external-links'; +import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; + +const TextBlock: React.FC<{ content: string }> = ({ content }) => { + const { t } = useSafeTranslation(); + const [isExpanded, setIsExpanded] = useState(false); + + const { preview, detail, hasNewlines } = useMemo(() => { + const hasNewlines = content.includes('\n'); + + if (!hasNewlines) { + return { preview: content, detail: '', hasNewlines: false }; + } + + const lines = content.split('\n'); + return { + preview: lines[0], + detail: lines.slice(1).join('\n'), + hasNewlines: true + }; + }, [content]); + + const buttonProps = { + size: 'xs' as const, + variant: 'ghost' as const, + colorScheme: 'blue' as const, + _hover: { bg: 'blue.50' } + }; + + return ( + + + {preview} + + + {hasNewlines && ( + <> + {!isExpanded && ( + + + + )} + + + + + {detail} + + + + + + + + )} + + ); +}; + +export default TextBlock; diff --git a/projects/app/src/components/Markdown/codeBlock/Tips.tsx b/projects/app/src/components/Markdown/codeBlock/Tips.tsx new file mode 100644 index 000000000..8f85da3ce --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/Tips.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Text, Flex } from '@chakra-ui/react'; +import Icon from '@fastgpt/web/components/common/Icon'; + +interface TipsProps { + content: string; + type?: 'error' | 'warning'; +} + +const Tips: React.FC = ({ content, type = 'error' }) => { + const isError = type === 'error'; + + return ( + + + + {content} + + + ); +}; + +export default Tips; diff --git a/projects/app/src/components/Markdown/img/EChartsCodeBlock.tsx b/projects/app/src/components/Markdown/img/EChartsCodeBlock.tsx index 2659d7c26..41046d85e 100644 --- a/projects/app/src/components/Markdown/img/EChartsCodeBlock.tsx +++ b/projects/app/src/components/Markdown/img/EChartsCodeBlock.tsx @@ -7,12 +7,42 @@ import { useMount } from 'ahooks'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useScreen } from '@fastgpt/web/hooks/useScreen'; +type EChartsGrid = { + top: string; + left: string; + bottom: string; + right: string; + containLabel: boolean; +}; + +type EChartsSeries = { + data: number[]; + name: string; + type: string; +}; + +type EChartsConfig = { + xAxis: { data: string[]; type: string }[]; + yAxis: { type: string }[]; + grid: EChartsGrid; + legend: { show: boolean }; + series: EChartsSeries[]; + tooltip: { trigger: string }; + dataZoom: unknown[]; +}; + const EChartsCodeBlock = ({ code }: { code: string }) => { const chartRef = useRef(null); const eChart = useRef(); const { isPc } = useSystem(); - const [option, setOption] = useState(); + const [option, setOption] = useState(); const [width, setWidth] = useState(400); + const [dataRange, setDataRange] = useState({ start: 0, end: 100 }); + const [totalDataLength, setTotalDataLength] = useState(0); + const [originalXData, setOriginalXData] = useState([]); + const [originalYData, setOriginalYData] = useState([]); + const dragStartTime = useRef(0); + const [isDragging, setIsDragging] = useState(false); const findMarkdownDom = useCallback(() => { if (!chartRef.current) return; @@ -29,39 +59,176 @@ const EChartsCodeBlock = ({ code }: { code: string }) => { return parent?.parentElement; }, [isPc]); + // filter data + const filterDataByRange = useCallback( + (originalXData: string[], originalYData: number[], range: { start: number; end: number }) => { + if (!originalXData.length || !originalYData.length) return { xData: [], yData: [] }; + + const totalLength = Math.min(originalXData.length, originalYData.length); + + const startIndex = Math.floor((range.start / 100) * totalLength); + const endIndex = Math.min(totalLength, Math.ceil((range.end / 100) * totalLength)); + + const actualEndIndex = Math.max(startIndex + 1, endIndex); + + // slice data + const filteredXData = originalXData.slice(startIndex, actualEndIndex); + const filteredYData = originalYData.slice(startIndex, actualEndIndex); + + return { xData: filteredXData, yData: filteredYData }; + }, + [] + ); + + // x and y data extraction + const extractXYData = useCallback((echartsConfig: EChartsConfig) => { + const emptyResult = { + xData: [] as string[], + yData: [] as number[], + chartContent: null as EChartsConfig | null + }; + + if (echartsConfig?.series?.length > 0 && echartsConfig?.xAxis?.length > 0) { + const series = echartsConfig.series[0]; + const xAxis = echartsConfig.xAxis[0]; + + return { + xData: xAxis.data || [], + yData: series.data || [], + chartContent: echartsConfig + }; + } + + return emptyResult; + }, []); + + // abstract chart render function + const createChartOption = useCallback( + (xData: string[], yData: number[], chartContent?: EChartsConfig | null) => { + if (chartContent) { + return { + ...chartContent, + xAxis: chartContent.xAxis.map((axis) => ({ + ...axis, + data: xData + })), + series: chartContent.series.map((series) => ({ + ...series, + data: yData + })) + }; + } + + // fallback to default config + return { + grid: { + bottom: '15%', + left: '5%', + right: '5%', + top: '10%', + containLabel: true + }, + xAxis: { + type: 'category', + data: xData, + boundaryGap: true, + axisTick: { + alignWithLabel: true, + interval: 0 + }, + axisLabel: { + interval: (() => { + const dataLength = xData.length; + if (dataLength <= 10) return 0; + if (dataLength <= 20) return 1; + if (dataLength <= 50) return Math.floor(dataLength / 10); + return Math.floor(dataLength / 15); + })(), + rotate: 45, + fontSize: 10, + formatter: (value: string) => { + return value && value.length > 20 ? `${value.substring(0, 20)}...` : value; + } + } + }, + yAxis: { + type: 'value', + scale: true + }, + series: [ + { + type: 'bar', + data: yData, + barCategoryGap: '20%', + itemStyle: { + borderRadius: [4, 4, 0, 0] + }, + name: 'Data' + } + ], + tooltip: { + trigger: 'axis', + formatter: function (params: Array<{ name: string; seriesName: string; value: number }>) { + if (Array.isArray(params) && params.length > 0) { + const param = params[0]; + return `${param.name}
${param.seriesName}: ${param.value}`; + } + return ''; + } + } + }; + }, + [] + ); + useMount(() => { // @ts-ignore import('echarts-gl'); }); + // generate and update chart option useLayoutEffect(() => { - const option = (() => { - try { - const parse = { - ...json5.parse(code.trim()), - toolbox: { - // show: true, - feature: { - saveAsImage: {} - } - } - }; + try { + const rawConfig: EChartsConfig = json5.parse(code.trim()); - return parse; - } catch (error) {} - })(); + const { xData, yData, chartContent } = extractXYData(rawConfig); - setOption(option ?? {}); - - if (!option) return; - - if (chartRef.current) { - try { - eChart.current = echarts.init(chartRef.current); - eChart.current.setOption(option); - } catch (error) { - console.error('ECharts render failed:', error); + if (xData.length === 0 || yData.length === 0) { + return; } + + setOriginalXData(xData); + setOriginalYData(yData); + setTotalDataLength(Math.min(xData.length, yData.length)); + + const { xData: filteredXData, yData: filteredYData } = filterDataByRange( + xData, + yData, + dataRange + ); + + const chartOption = createChartOption(filteredXData, filteredYData, chartContent); + + // Add toolbox for image saving + const RenderOption = { + ...chartOption, + toolbox: { + feature: { + saveAsImage: {} + } + } + }; + + setOption(RenderOption as EChartsConfig); + + if (chartRef.current) { + if (!eChart.current) { + eChart.current = echarts.init(chartRef.current); + } + eChart.current.setOption(RenderOption); + } + } catch (error) { + console.error('ECharts render failed:', error); } findMarkdownDom(); @@ -69,25 +236,159 @@ const EChartsCodeBlock = ({ code }: { code: string }) => { return () => { if (eChart.current) { eChart.current.dispose(); + eChart.current = undefined; } }; - }, [code, findMarkdownDom]); + }, [code, findMarkdownDom, filterDataByRange, dataRange, createChartOption, extractXYData]); const { screenWidth } = useScreen(); useEffect(() => { findMarkdownDom(); - }, [screenWidth]); + }, [screenWidth, findMarkdownDom]); useEffect(() => { eChart.current?.resize(); }, [width]); + // slider control + const handleRangeChange = useCallback((newRange: { start: number; end: number }) => { + setDataRange(newRange); + }, []); + // handle drag + const handleDrag = useCallback( + (type: 'left' | 'right' | 'range', e: React.MouseEvent) => { + e.preventDefault(); + + setIsDragging(false); + dragStartTime.current = Date.now(); + const startX = e.clientX; + const { start: startValue, end: endValue } = dataRange; + const rangeWidth = endValue - startValue; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaX = Math.abs(moveEvent.clientX - startX); + const timeDelta = Date.now() - dragStartTime.current; + + if (deltaX > 5 || timeDelta > 100) { + setIsDragging(true); + } + + const deltaPercent = ((moveEvent.clientX - startX) / Math.max(width, 400)) * 100; + + // drag handle + if (type === 'left') { + const newStart = Math.max(0, Math.min(startValue + deltaPercent, endValue)); + handleRangeChange({ start: newStart, end: endValue }); + } else if (type === 'right') { + const newEnd = Math.min(100, Math.max(endValue + deltaPercent, startValue)); + handleRangeChange({ start: startValue, end: newEnd }); + } else if (type === 'range') { + const newStart = Math.max(0, Math.min(startValue + deltaPercent, 100 - rangeWidth)); + handleRangeChange({ start: newStart, end: newStart + rangeWidth }); + } + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + setTimeout(() => setIsDragging(false), 100); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }, + [dataRange, width, handleRangeChange] + ); + return ( {!option && ( )} + + {/* data range slider */} + {option && totalDataLength > 1 && ( + + { + if (!isDragging) { + const rect = e.currentTarget.getBoundingClientRect(); + const percentage = ((e.clientX - rect.left) / rect.width) * 100; + const halfWidth = 10; + + const newStart = Math.max(0, Math.min(percentage - halfWidth, 80)); + const newEnd = Math.min(100, Math.max(percentage + halfWidth, 20)); + handleRangeChange({ start: newStart, end: newEnd }); + } + }} + > + {/* data thumbnail */} + {originalYData.length > 0 && ( + + { + const maxVal = Math.max(...originalYData); + const minVal = Math.min(...originalYData); + const range = maxVal - minVal || 1; + + return originalYData + .map((value, index) => { + const x = (index / (originalYData.length - 1)) * (width - 8) + 4; + const y = 36 - ((value - minVal) / range) * 32; + return `${x},${y}`; + }) + .join(' '); + })()} + fill="none" + stroke="#93c5fd" + strokeWidth="1.5" + opacity="0.7" + /> + + )} + + {/* select area */} + handleDrag('range', e)} + /> + + {/* left and right drag handle */} + handleDrag('left', e)} + /> + handleDrag('right', e)} + /> + + + )} ); }; diff --git a/projects/app/src/components/Markdown/index.tsx b/projects/app/src/components/Markdown/index.tsx index 5445a8168..ce0f1f082 100644 --- a/projects/app/src/components/Markdown/index.tsx +++ b/projects/app/src/components/Markdown/index.tsx @@ -1,19 +1,22 @@ +import 'katex/dist/katex.min.css'; import React, { useCallback, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; -import 'katex/dist/katex.min.css'; -import RemarkMath from 'remark-math'; // Math syntax -import RemarkBreaks from 'remark-breaks'; // Line break -import RehypeKatex from 'rehype-katex'; // Math render -import RemarkGfm from 'remark-gfm'; // Special markdown syntax import RehypeExternalLinks from 'rehype-external-links'; +import RehypeKatex from 'rehype-katex'; // Math render +import RehypeRaw from 'rehype-raw'; +import RemarkBreaks from 'remark-breaks'; // Line break +import RemarkGfm from 'remark-gfm'; // Special markdown syntax +import RemarkMath from 'remark-math'; // Math syntax -import styles from './index.module.scss'; import dynamic from 'next/dynamic'; +import styles from './index.module.scss'; import { Box } from '@chakra-ui/react'; -import { CodeClassNameEnum, mdTextFormat } from './utils'; import { useCreation } from 'ahooks'; import type { AProps } from './A'; +import { CodeClassNameEnum } from './utils'; + +import DomPurify from 'dompurify'; const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false }); const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false }); @@ -23,11 +26,19 @@ const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { ssr: false }); const VideoBlock = dynamic(() => import('./codeBlock/Video'), { ssr: false }); const AudioBlock = dynamic(() => import('./codeBlock/Audio'), { ssr: false }); +const TableBlock = dynamic(() => import('./codeBlock/Table'), { ssr: false }); +const IndicatorCard = dynamic(() => import('./codeBlock/IndicatorCard'), { ssr: false }); +const LinkBlock = dynamic(() => import('./codeBlock/Link'), { ssr: false }); +const Tips = dynamic(() => import('./codeBlock/Tips'), { ssr: false }); +const Divider = dynamic(() => import('./codeBlock/Divider'), { ssr: false }); +const TextBlock = dynamic(() => import('./codeBlock/TextBlock'), { ssr: false }); const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false }); const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false }); const A = dynamic(() => import('./A'), { ssr: false }); +const formatCodeBlock = (lang: string, content: string) => `\`\`\`${lang}\n${content}\n\`\`\``; + type Props = { source?: string; showAnimation?: boolean; @@ -68,10 +79,79 @@ const MarkdownRender = ({ }; }, [chatAuthData, onOpenCiteModal, showAnimation]); + // convert single item to Markdown + const convertRenderBlockToMarkdown = useCallback((jsonContent: string): string => { + const converItem = (type: string, content: any) => { + switch (type) { + case 'TEXT': + return (typeof content === 'string' ? content : JSON.stringify(content)) + '\n\n'; + + case 'CHART': + return content?.hasChart && content?.echartsData + ? `\`\`\`echarts\n${JSON.stringify(content.echartsData, null, 2)}\n\`\`\`\n\n` + : ''; + + case 'TABLE': + return content?.data + ? `\`\`\`table\n${JSON.stringify(content.data, null, 2)}\n\`\`\`\n\n` + : ''; + + case 'INDICATOR': + return content?.dataList + ? `\`\`\`indicator\n${JSON.stringify(content.dataList, null, 2)}\n\`\`\`\n\n` + : ''; + + case 'LINK': + return content?.text && content?.url + ? `\`\`\`link\n${JSON.stringify(content, null, 2)}\n\`\`\`\n\n` + : ''; + + case 'ERROR_TIPS': + return content ? `\`\`\`error_tips\n${content}\n\`\`\`\n\n` : ''; + + case 'WARNING_TIPS': + return content ? `\`\`\`warning_tips\n${content}\n\`\`\`\n\n` : ''; + + case 'DIVIDER': + return `\`\`\`divider\n\n\`\`\`\n\n`; + + case 'TEXTBLOCK': + return content ? `\`\`\`textblock\n${content}\n\`\`\`\n\n` : ''; + + default: + return formatCodeBlock('json', jsonContent); + } + }; + try { + const jsonObj = JSON.parse(jsonContent); + if (Array.isArray(jsonObj)) { + return jsonObj.map((item) => converItem(item.type, item.content)).join(`\n\n`); + } else { + return converItem(jsonObj.type, jsonObj.content); + } + } catch { + return formatCodeBlock('json', jsonContent); + } + }, []); + const formatSource = useMemo(() => { if (showAnimation || forbidZhFormat) return source; - return mdTextFormat(source); - }, [forbidZhFormat, showAnimation, source]); + + const result = source.replace(/```RENDER([\s\S]*?)```/g, (match, p1) => { + // p1: the content inside ```RENDER ... ``` + const cleanedContent = p1 + .replace(/^```[\s\S]*?(\n)?/, '') + .replace(/```$/, '') + .trim(); + return convertRenderBlockToMarkdown(cleanedContent); + }); + + return result; + }, [convertRenderBlockToMarkdown, forbidZhFormat, showAnimation, source]); + + const sanitizedSource = useMemo(() => { + return DomPurify.sanitize(formatSource); + }, [formatSource]); const urlTransform = useCallback((val: string) => { return val; @@ -81,14 +161,38 @@ const MarkdownRender = ({ - {formatSource} + {sanitizedSource} {isDisabled && } @@ -134,6 +238,27 @@ function Code(e: any) { if (codeType === CodeClassNameEnum.audio) { return ; } + if (codeType === CodeClassNameEnum.table) { + return ; + } + if (codeType === CodeClassNameEnum.indicator) { + return ; + } + if (codeType === CodeClassNameEnum.link) { + return ; + } + if (codeType === CodeClassNameEnum.error_tips) { + return ; + } + if (codeType === CodeClassNameEnum.warning_tips) { + return ; + } + if (codeType === CodeClassNameEnum.divider) { + return ; + } + if (codeType === CodeClassNameEnum.textblock) { + return ; + } return ( diff --git a/projects/app/src/components/Markdown/utils.ts b/projects/app/src/components/Markdown/utils.ts index 544585bba..686619677 100644 --- a/projects/app/src/components/Markdown/utils.ts +++ b/projects/app/src/components/Markdown/utils.ts @@ -10,7 +10,14 @@ export enum CodeClassNameEnum { html = 'html', svg = 'svg', video = 'video', - audio = 'audio' + audio = 'audio', + table = 'table', + indicator = 'indicator', + link = 'link', + error_tips = 'error_tips', + warning_tips = 'warning_tips', + divider = 'divider', + textblock = 'textblock' } export const mdTextFormat = (text: string) => {